setup of IB
This commit is contained in:
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal 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
8
README.md
Normal 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
|
9
next.config.mjs
Normal file
9
next.config.mjs
Normal 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
7632
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
62
package.json
Normal file
62
package.json
Normal 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
14
postcss.config.cjs
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
33
src/app/_components/error/http-error.module.css
Normal file
33
src/app/_components/error/http-error.module.css
Normal 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);
|
||||||
|
}
|
14
src/app/_components/error/http-error.tsx
Normal file
14
src/app/_components/error/http-error.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
52
src/app/_components/success-component/SuccessMessage.tsx
Normal file
52
src/app/_components/success-component/SuccessMessage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
45
src/app/_components/user-context.tsx
Normal file
45
src/app/_components/user-context.tsx
Normal 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 }
|
39
src/app/_data/entities/Ticket.ts
Normal file
39
src/app/_data/entities/Ticket.ts
Normal 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
|
||||||
|
|
||||||
|
}
|
27
src/app/_data/entities/User.ts
Normal file
27
src/app/_data/entities/User.ts
Normal 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
|
||||||
|
|
||||||
|
}
|
3
src/app/_data/samples/users.csv
Normal file
3
src/app/_data/samples/users.csv
Normal 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
|
|
44
src/app/_data/source/app-data-source.ts
Normal file
44
src/app/_data/source/app-data-source.ts
Normal 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;
|
17
src/app/_themes/KccbTheme.ts
Normal file
17
src/app/_themes/KccbTheme.ts
Normal 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'
|
||||||
|
});
|
6
src/app/_types/accountNo.ts
Normal file
6
src/app/_types/accountNo.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
type accountNo = {
|
||||||
|
id: string
|
||||||
|
bank_account_no?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default accountNo
|
12
src/app/_util/proper.ts
Normal file
12
src/app/_util/proper.ts
Normal 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(' ')
|
||||||
|
}
|
34
src/app/_util/validation.ts
Normal file
34
src/app/_util/validation.ts
Normal 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
10
src/app/global.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* border: 1px solid black; */
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
80
src/app/home/layout.module.css
Normal file
80
src/app/home/layout.module.css
Normal 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
159
src/app/home/layout.tsx
Normal 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>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
80
src/app/home/page.module.css.bkup
Normal file
80
src/app/home/page.module.css.bkup
Normal 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
18
src/app/home/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
263
src/app/home/user/raised-ticket/page.tsx
Normal file
263
src/app/home/user/raised-ticket/page.tsx
Normal 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;
|
332
src/app/home/user/view-ticket/page.tsx
Normal file
332
src/app/home/user/view-ticket/page.tsx
Normal 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
BIN
src/app/image/ebanking.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
BIN
src/app/image/ib_front_page.jpg
Normal file
BIN
src/app/image/ib_front_page.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 261 KiB |
BIN
src/app/image/media.jpg
Normal file
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
36
src/app/layout.tsx
Normal 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
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
195
src/app/login/otp/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
29
src/app/login/page.module.css
Normal file
29
src/app/login/page.module.css
Normal 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
126
src/app/login/page.tsx
Normal 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
7
src/app/page.tsx
Normal 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
22
src/app/providers.tsx
Normal 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
41
tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
Reference in New Issue
Block a user