setup of IB
This commit is contained in:
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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user