setup of IB

This commit is contained in:
2025-06-25 15:21:16 +05:30
commit 0420c72aa7
38 changed files with 9493 additions and 0 deletions

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

40
.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
/Docs/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# .vscode
.vscode

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
- npx create-next-app@latest ib --typescript
- cd ib
- npm install @mantine/core @mantine/hooks
- npm run build
- npm run start
- npm run dev

1
TODO.md Normal file
View File

@@ -0,0 +1 @@
# Todo

9
next.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: ["typeorm", "knex"],
},
reactStrictMode: true
};
export default nextConfig;

7632
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

62
package.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "IB",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@mantine/charts": "^7.11.1",
"@mantine/core": "^7.8.1",
"@mantine/dates": "^7.8.1",
"@mantine/form": "^7.11.0",
"@mantine/hooks": "^7.8.1",
"@mantine/notifications": "^7.8.1",
"@tabler/icons-react": "^3.28.1",
"@tanstack/react-query": "^5.32.0",
"axios": "^1.6.8",
"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",
"iron-session": "^8.0.1",
"luxon": "^3.4.4",
"natural": "^6.10.5",
"next": "^14.2.23",
"oracledb": "^6.5.0",
"pg": "^8.11.5",
"pg-query-stream": "^4.5.5",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.3",
"reflect-metadata": "^0.2.2",
"sharp": "^0.33.5",
"typeorm": "^0.3.20",
"validator": "^13.11.0",
"winston": "^3.13.0"
},
"devDependencies": {
"@types/damerau-levenshtein": "^1.0.2",
"@types/luxon": "^3.4.2",
"@types/node": "^20",
"@types/pg": "^8.11.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/validator": "^13.11.9",
"eslint": "^8",
"eslint-config-next": "14.2.1",
"eslint-config-prettier": "^10.0.1",
"postcss": "^8.4.38",
"postcss-preset-mantine": "^1.15.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5"
}
}

14
postcss.config.cjs Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};

View File

@@ -0,0 +1,33 @@
.label {
text-align: center;
font-weight: 900;
font-size: rem(220px);
line-height: 1;
margin-bottom: calc(var(--mantine-spacing-xl) * 1.5);
@media (max-width: $mantine-breakpoint-sm) {
font-size: rem(120px);
}
}
.title {
font-family:
Greycliff CF,
var(--mantine-font-family);
text-align: center;
font-weight: 900;
font-size: rem(38px);
color: var(--mantine-color-white);
@media (max-width: $mantine-breakpoint-sm) {
font-size: rem(32px);
}
}
.description {
max-width: rem(540px);
margin: auto;
margin-top: var(--mantine-spacing-xl);
margin-bottom: calc(var(--mantine-spacing-xl) * 1.5);
color: var(--mantine-color-blue-1);
}

View File

@@ -0,0 +1,14 @@
import { Title, Text, Button, Container, Group } from '@mantine/core';
import classes from './http-error.module.css';
export function HttpError(props: { status: number, code: string, message: string }) {
return (
<Container>
<div className={classes.label}>{props.status}</div>
<Title className={classes.title}>{props.code}</Title>
<Text size="lg" ta="center" className={classes.description}>
{props.message}
</Text>
</Container>
);
}

View File

@@ -0,0 +1,52 @@
import React from "react";
import { Box, Center, ActionIcon, Text } from "@mantine/core";
import { IconCircleDashedCheck, IconX } from "@tabler/icons-react";
interface SuccessMessageProps {
username: string;
onClose: () => void;
}
export default function SuccessMessage({
username,
onClose,
}: SuccessMessageProps) {
return (
<Center style={{ height: "400px" }}>
<Box
style={{
width: "375px",
height: "375px",
backgroundColor: "#f0f0f0",
borderRadius: "8px",
position: "relative",
boxShadow: "0px 4px 10px rgba(0,0,0,0.1)",
justifyContent: "center",
textAlign: "center",
}}
>
<ActionIcon
onClick={onClose}
style={{
position: "absolute",
top: "10px",
left: "10px",
}}
>
<IconX size={24} />
</ActionIcon>
<Center
style={{
flexDirection: "column",
height: "100%",
}}
>
<IconCircleDashedCheck size={100} color="green" />
<Text mt="lg" size="lg" style={{ fontWeight: 500, align: "center" }}>
New User <b>{username}</b> created Successfully
</Text>
</Center>
</Box>
</Center>
);
}

View File

@@ -0,0 +1,45 @@
"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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,3 @@
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

@@ -0,0 +1,44 @@
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;

View File

@@ -0,0 +1,17 @@
"use client";
import { MantineColorsTuple, createTheme } from "@mantine/core";
const KccbColors: MantineColorsTuple = [
"#e3f2fd", "#bbdefb", "#90caf9", "#64b5f6", "#42a5f5",
"#2196f3", "#1e88e5", "#1976d2", "#1565c0", "#0d47a1"
];
export const KccbTheme = createTheme({
/* Put your mantine theme override here */
primaryColor: 'kccb-colors',
colors: {
'kccb-colors': KccbColors
}
// primaryColor: 'kccb-colors'
});

View File

@@ -0,0 +1,6 @@
type accountNo = {
id: string
bank_account_no?: string,
}
export default accountNo

12
src/app/_util/proper.ts Normal file
View File

@@ -0,0 +1,12 @@
export default function getProper(resourceName: string | undefined) {
if (!resourceName || resourceName === '') return '';
return resourceName
.toLowerCase()
.replace('kccb', '')
.replaceAll(/\W+/g, '') // replace all non alpha-numeric
.split('_')
.map(word => word.trim())
.filter(word => word !== '')
.map(word => word[0].toUpperCase() + word.substring(1))
.join(' ')
}

View File

@@ -0,0 +1,34 @@
export function isValidDate(dateString: string): boolean {
// Regular expression to match dd-mm-yyyy format
const regex = /^(0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-(\d{4})$/;
if (!regex.test(dateString)) {
return false;
}
const [day, month, year] = dateString.split('-').map(Number);
const date = new Date(year, month - 1, day);
return date.getFullYear() === year &&
date.getMonth() === month - 1 &&
date.getDate() === day;
}
/*Write code for password format
password shouls be atleast 5 charecter
Password should be have one special charecter and one numeric value*/
export function validatePassword(password: string): boolean {
if (password.length < 5) {
return false; //password should be atleast 5 charector long
}
const specialCharPattern = /[!@#$%^&*(),.?":{}|<>]/; // checking at least one special character
const numericPattern = /[0-9]/; //at least one numeric character
if (!specialCharPattern.test(password)) {
return false;
}
if (!numericPattern.test(password)) {
return false;
}
return true;
}

10
src/app/global.css Normal file
View File

@@ -0,0 +1,10 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
/* border: 1px solid black; */
}
html, body {
height: 100vh;
}

View File

@@ -0,0 +1,80 @@
.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);
}

159
src/app/home/layout.tsx Normal file
View File

@@ -0,0 +1,159 @@
"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

@@ -0,0 +1,80 @@
.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);
}

18
src/app/home/page.tsx Normal file
View File

@@ -0,0 +1,18 @@
"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

@@ -0,0 +1,263 @@
"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

@@ -0,0 +1,332 @@
"use client";
import {
ActionIcon,
Button,
Container,
Flex,
Group,
Modal,
Space,
Table,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconDisabled, IconEye, IconMessage, IconSearch, IconTrash ,IconArrowsUpDown} from "@tabler/icons-react";
import axios, { AxiosError } from "axios";
import { useEffect, useState } from "react";
import { notifications } from "@mantine/notifications";
interface Message {
note: string;
time:string;
}
interface Ticket {
ticket_id: string;
category_of_request: string;
nature_of_request: string;
created_date: string;
status: string;
message: Message[];
}
interface TicketDetails {
ticket_id: string;
category_of_request: string;
nature_of_request: string;
additional_info: string;
message : string;
created_date: string;
}
export default function Page() {
const [opened, { open, close }] = useDisclosure(false);
const [tickets, setTickets] = useState<Ticket[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
const [activeMessages, setActiveMessages] = useState<Message[] | null>(null);
const [ticketDetails, setTicketDetails] = useState<TicketDetails| null>(null);
const [ticketStatus, setTicketStatus] = useState<string| null>(null);
const [sortOrder,setSortOrder]= useState<"asc" |"desc">("desc");
const [detailsOpened, { open: openDetails, close: closeDetails }] = useDisclosure(false);
// For view ticket details
const handleOpenDetails = async (ticketId: string) => {
try {
const response = await axios.get<TicketDetails>(`/api/ticket/${ticketId}`); // Assuming API provides details
setTicketDetails(response.data);
openDetails();
}
catch(error: AxiosError | any) {
console.error("Failed to fetch ticket details:", error);
notifications.show({
color: 'red',
title: error.response.status,
message: error.response.data.message
})
}
};
// For parsing additional_info
const parseAdditionalInfo = (info: string) => {
return info.split(", ").map((item) => {
const [key, value] = item.split(": ");
return { key: key.trim(), value: value?.trim() || "" };
});
};
// For delete a ticket
const handleDelete = async (ticketId: string) => {
const isConfirm =window.confirm("Do you want to delete the ticket?");
if(!isConfirm)
return;
try {
const response = await axios.delete<TicketDetails>(`/api/ticket/${ticketId}`); // Assuming API provides details
const data = await response.data;
alert(data.message);
}
catch(error: AxiosError | any) {
console.error("Failed to delete ticket:", error);
notifications.show({
color: 'red',
title: error.response.status,
message: error.response.data.message
})
}
finally{
window.location.reload();
}
};
// Sort and filter tickets based on the time
useEffect(() => {
const sortedTickets = [...tickets].sort(
// (a, b) => new Date(b.created_date).getTime() - new Date(a.created_date).getTime()
(a, b) => parseInt(b.ticket_id) - parseInt(a.ticket_id)
);
const results = sortedTickets.filter((ticket) =>
ticket.category_of_request.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredTickets(results);
}, [searchQuery, tickets]);
// Fetch tickets from API
useEffect(() => {
async function fetchTickets() {
try {
const response = await axios.get<Ticket[]>("/api/ticket");
setTickets(response.data);
} catch (error) {
console.error("Failed to fetch tickets: ", error);
}
}
fetchTickets();
}, []);
// Sorting the ticket when any ticket resolved the go to the end of the list
useEffect(() => {
const sortedTickets = [...tickets].sort((a, b) => {
if (a.status.toLowerCase() === "resolved" && b.status.toLowerCase() !== "resolved") return 1;
if (b.status.toLowerCase() === "resolved" && a.status.toLowerCase() !== "resolved") return -1;
return parseInt(b.ticket_id) - parseInt(a.ticket_id); // Sort by ticket ID otherwise
});
const results = sortedTickets.filter(ticket =>
ticket.category_of_request.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredTickets(results);
}, [searchQuery, tickets]);
// For sorting ticket history according to time
const sortedMessages = activeMessages
? [...activeMessages].sort((a, b) => {
const timeA = new Date(a.time).getTime();
const timeB = new Date(b.time).getTime();
return sortOrder === "asc" ? timeA - timeB : timeB - timeA;
})
: [];
const handleOpenMessage = (messages: Message[],status:string) => {
console.log(messages);
setActiveMessages(messages);
setTicketStatus(status);
open();
};
//Add conditional row styles
const getRowStyle = (status: string) => {
switch (status.toLowerCase()) {
case "resolved":
// return { backgroundColor: "#d9d9d9", color: "#a6a6a6"}; // Grey for closed
return {color: "#a6a6a6"}; // Grey for closed
default:
return {};
}
};
const rows = filteredTickets.map((ticket) => (
<Table.Tr key={ticket.ticket_id} style={getRowStyle(ticket.status)}> {/* added for coloured grey */}
{/* <Table.Tr key={ticket.ticket_id}> */}
<Table.Td>{ticket.ticket_id}</Table.Td>
<Table.Td>{ticket.category_of_request}</Table.Td>
<Table.Td>{ticket.nature_of_request}</Table.Td>
<Table.Td>{ticket.created_date}</Table.Td>
<Table.Td>{ticket.status}</Table.Td>
<Table.Td>
<Group>
<ActionIcon style={getRowStyle(ticket.status)}
variant="subtle" color="blue"
onClick={() => handleOpenMessage(ticket.message,ticket.status)}
>
<Tooltip label="Ticket History">
<IconMessage />
</Tooltip>
{/* design for badge */}
</ActionIcon>
</Group>
</Table.Td>
<Table.Td>
<Group gap="md" wrap="nowrap" justify="center">
<ActionIcon style={getRowStyle(ticket.status)}
variant="subtle"
onClick={() => handleOpenDetails(ticket.ticket_id)}>
<Tooltip label="Details of the Ticket">
<IconEye />
</Tooltip>
</ActionIcon>
{/* Remove delete icon */}
{/* <ActionIcon style={getRowStyle(ticket.status)}
disabled={ticket.status.toLowerCase()==='resolved'}
variant="subtle" color="red"
onClick={() => handleDelete(ticket.ticket_id)}>
<Tooltip label="Delete the Ticket">
<IconTrash/>
</Tooltip>
</ActionIcon> */}
</Group>
</Table.Td>
</Table.Tr>
));
return (
<Container fluid>
<Title order={3}>View Ticket</Title>
<Space h="1rem" />
<Flex align="center" justify="space-between" w={720}>
<Flex align="center">
<Title order={5}>Tickets</Title>
<Space w="1rem" />
<TextInput
placeholder="Search"
value={searchQuery}
onChange={(event) => setSearchQuery(event.currentTarget.value)}
radius="md"
w={250}
leftSection={<IconSearch size={16} />}
/>
</Flex>
</Flex>
<Space h="1.5rem" />
<Table
w={720}
stickyHeader
stickyHeaderOffset={60}
withTableBorder
highlightOnHover
horizontalSpacing="lg"
>
<Table.Thead>
<Table.Tr>
<Table.Th>Ticket ID</Table.Th>
<Table.Th>Category</Table.Th>
<Table.Th>Description</Table.Th>
<Table.Th>Timestamp</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>History</Table.Th>
<Table.Th>Action</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
<Modal opened={opened} onClose={close} title="">
<Flex justify="space-between" align="center" >
<Title order={4}>Ticket History</Title>
<ActionIcon
variant="subtle"
onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
>
<Tooltip label={`Sort by ${sortOrder === "asc" ? "Newest " : "Oldest"}`} position="right">
<IconArrowsUpDown size={18} />
</Tooltip>
</ActionIcon>
</Flex>
<Flex direction="column" gap="sm" style={{ maxHeight: "400px", overflowY: "auto" }}>
{sortedMessages?.map((msg, index) => (
<div
key={index}
style={{
padding: "10px",
background: index % 2 === 0 ? "#f1f3f5" : "#dce6f2",
borderRadius: "10px",
maxWidth: "80%",
alignSelf: "flex-start",
boxShadow: "0 2px 5px rgba(0, 0, 0, 0.1)",
}}
>
<p style={{ margin: 0, fontSize: "14px", color: "black",textAlign: "left" }}>{msg.note}</p>
{/* //Fetch the note IST time */}
<p style={{ margin: "6px 0 0", fontSize: "10px", color: "grey", textAlign: "right" }}>Internal team send by {new Date(Number(msg.time)*1000).toLocaleString('en-IN',{timeZone: 'Asia/Kolkata'})}</p>
</div>
))}
{ticketStatus?.toLowerCase()=== "resolved" && (<div style={{ textAlign: "center", fontSize: "14px", color: "red", marginTop: "10px" }}>
<p>----- Ended Chat ------</p>
</div>)}
{/* Fixed Close Button */}
<div
style={{
position: "sticky",
bottom: 0,
background: "white",
padding: "10px",
textAlign: "center",
boxShadow: "0px -5px 4px rgb(255, 255, 255)",
}}
>
<Button onClick={close}>Close</Button>
</div>
</Flex>
</Modal>
<Modal opened={detailsOpened} onClose={closeDetails}>
{ticketDetails ? (
<Flex direction="column" gap="md">
<div><strong>Ticket ID:</strong> {ticketDetails.ticket_id}</div>
<div><strong>Category of Complaint:</strong> {ticketDetails.category_of_request}</div>
<div><strong>Description:</strong> {ticketDetails.nature_of_request}</div>
{/* <div><strong>Additional Information:</strong> {ticketDetails.additional_info}</div> */}
{/* Additional Info Table */}
<div><strong>Additional Info:</strong></div>
<Table withTableBorder withColumnBorders highlightOnHover>
<Table.Tbody>
{parseAdditionalInfo(ticketDetails.additional_info).map((item, index) => (
<Table.Tr key={index}>
<Table.Td>{item.key}</Table.Td>
<Table.Td>{item.value}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
<div><strong>Message:</strong> {ticketDetails.message}</div>
<div><strong>Created Date:</strong> {ticketDetails.created_date}</div>
<Button onClick={closeDetails} mt="sm">Close</Button>
</Flex>
) : (
<p>Loading details...</p>
)}
</Modal>
</Container>
);
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

36
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,36 @@
import "@/app/global.css";
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/charts/styles.css';
import "reflect-metadata";
import type { Metadata } from "next";
import { Providers } from "./providers";
import { ColorSchemeScript, Container } from "@mantine/core";
export const metadata: Metadata = {
title: "KCCB Internet Banking",
description: "KCCB IB",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<ColorSchemeScript />
{/* <link rel="shortcut icon" href="/favicon.svg" /> */}
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
/>
</head>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}

BIN
src/app/login/helpdesk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

195
src/app/login/otp/page.tsx Normal file
View File

@@ -0,0 +1,195 @@
"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

@@ -0,0 +1,29 @@
.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;
}
}

126
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,126 @@
"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";
import { Providers } from "@/app/providers";
export default function Demo() {
return (
<Providers>
<div
style={{
backgroundColor: "#f8f9fa",
padding: "20px",
fontFamily: "sans-serif",
height: '100vh',
// border: "solid black"
}}
>
<Image
radius="md"
fit="cover"
src={myImage}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "12%" }}
/>
{/* For Text */}
{/* <Paper shadow="sm" p="lg" withBorder style={{ maxWidth: 1000, margin: "auto" }}>
<Text
style={{
fontSize: "20px",
textAlign: "center",
marginBottom: "20px",
fontWeight: "bold",
}}
>
KCC Bank welcomes you to KCCB - Internet Banking Services
</Text>
</Paper> */}
{/* Security Message */}
<Box style={{ width: "100%", overflow: "hidden", whiteSpace: "nowrap", padding: "8px 0" }}>
<Text
component="span"
style={{
display: "inline-block", paddingLeft: "100%", animation: "scroll-left 60s linear infinite", fontWeight: "bold", color: "#004d99"
}}>
Always login to our Net Banking site directly or through Banks website.
Do not disclose your User Id and Password to any third party and keep Your UserId and Password strictly confidential.
KCC Bank never asks for UserId,Passwords and Pins through email or phone.
Be ware of Phishing mails with links to fake bank's websites asking for personal information are in circulation.
⚠️ Please DO NOT Click on the links given in the emails asking for personal details like bank account number, userID and password.
⚠️ If you had shared your User Id and Password through such mails or links, please change your Password immediately.
⚠️ Inform the Bank/branch in which your account is maintained for resetting your password.
</Text>
<style>
{`
@keyframes scroll-left {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
`}
</style>
</Box>
<div style={{ display: "flex", height: "70vh" }}>
<div style={{ flex: 1, backgroundColor: "#f0f0f0", display: "flex" }}>
<Image
radius="md"
fit="cover"
src={frontPage}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "100%" }}
/>
</div>
<div style={{ flex: 1, backgroundColor: "#c1e0f0", display: "flex", justifyContent: 'center', alignItems: 'center' }}>
<Card shadow="lg" padding="xl" radius="md" style={{ width: 480, height: 400, backgroundColor: '#f0f0f0', boxShadow: '#f0f0f0' }}>
<Title order={5} style={{ marginLeft: 10 }}>Welcome To KCC Bank</Title>
<Title order={1} style={{ marginLeft: 10, color: "#000066" }}>Internet Banking</Title>
{/* <Box style={{ width: "400px", height: "200px", margin: "10px 0 20px 10px", border: "1px solid black", marginLeft: 30 }}>
<form style={{ marginLeft: 10, width: "100px", height: "50px" }}>
<Flex gap="sm" align="center" justify="flex-start" style={{border: "solid blue",width:"300%"}}>
<Text style={{border:"solid black"}}>CIF Number</Text>
<TextInput
placeholder="Enter your CIF no"
style={{ flex: 1 }}
/>
<Text style={{border:"solid black"}}>Password : </Text>
</Flex>
</form>
</Box> */}
<Box style={{ width: "370px", padding: "20px", borderRadius: "12px", boxShadow: "0 2px 10px rgba(0,0,0,0.1)", border: "1px solid black", marginLeft: 30 }}>
<Flex justify="center" gap="md" >
<form>
<TextInput
label="CIF Number"
placeholder="Enter your CIF no"
style={{ flex: 1 }}
/>
<br></br>
<TextInput
label="Password"
placeholder="Enter your password"
style={{ flex: 1 }}
/>
<br></br>
<Button fullWidth variant="filled" mt="md">Login</Button>
</form>
</Flex>
</Box>
</Card>
</div>
</div>
</div>
</Providers>
);
}

7
src/app/page.tsx Normal file
View File

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

22
src/app/providers.tsx Normal file
View File

@@ -0,0 +1,22 @@
'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';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<MantineProvider theme={KccbTheme} defaultColorScheme='light'>
<Notifications position='top-center' />
<QueryClientProvider client={queryClient}>
<UserContextProvider>
{children}
</UserContextProvider>
</QueryClientProvider>
</MantineProvider>
)
}

41
tsconfig.json Normal file
View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
, "src/casbin/test.js", "src/app/home/admin/reset-user-password" ],
"exclude": [
"node_modules"
]
}