first commit-CRM_module

This commit is contained in:
Tomosa Sarkar 2025-01-29 13:27:23 +05:30
commit c8b26fa46b
52 changed files with 10407 additions and 0 deletions

3
.eslintrc.json Normal file
View File

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

39
.gitignore vendored Normal file
View File

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

110
API.md Normal file
View File

@ -0,0 +1,110 @@
## CRM (Customer Relationship Manager)
### - Create a login page by Account No :
POST api/auth/login/account_no
Input :
```
{"AccNo" : "XXXXXXXXXXXXX"}
```
Output :
```
{
"ok": true
}
```
### - Fetch the mobile no by Account No :
GET api/mobile_no/by/[Account Number]
Output:
```
"mobile_number": "XXXXXXXXXX"
```
### - Generate OTP and sended to the mobile no:
GET api/otp
Output:
```
<!-- {"message":"New OTP has been sent to your register mobile number xxxxxxx527"} -->
{
"message": "46687"
}
```
### - Validate the OTP :
POST api/otp
Body
```
{"OTP" : XXXXX}
```
Output
```
{"ok":true}
```
### - Post the complaint request
POST http://localhost:3000/api/ticket
Input :
```
{
"summary": "UPI Fault",
"description": "UPI Fault",
"steps_to_reproduce" :"Money got debited from account"
}
```
Output :
```
"message": "Ticket No:2 created successfully"
```
### - Get the request By the user
GET http://localhost:3000/api/ticket
Output :
```
[
{
"id": 1,
"category_of_request": "Fund Transfer failure",
"nature_of_request": "Fund Transfer failure",
"created_date": "Mon Jan 13 2025 16:32:30 GMT+0530 (India Standard Time)",
"Status": "Reopen",
"Message": [
{
"note": "solve"
},
{
"note": "check once"
},
{
"note": "in progress"
}
]
},
{
"id": 2,
"category_of_request": "UPI Fault",
"nature_of_request": "UPI Fault",
"created_date": "Tue Jan 14 2025 11:11:03 GMT+0530 (India Standard Time)",
"Status": "Open",
"Message": [
{
"note": "note1"
}
]
}
]
```

View File

@ -0,0 +1,160 @@
{
"info": {
"_postman_id": "c0971b0e-8469-4ffd-a95e-090b1ba552da",
"name": "CRM",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "39926215"
},
"item": [
{
"name": "Login",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"AccNo\" :\"30022497138\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://localhost:3000/api/auth/login/account_no"
},
"response": []
},
{
"name": "get account details",
"request": {
"auth": {
"type": "inherit",
"inherit": {}
},
"method": "GET",
"header": [],
"url": "http://localhost:3000/api/user"
},
"response": []
},
{
"name": "Validate OTP",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"OTP\": 77208}",
"options": {
"raw": {
"language": "text"
}
}
},
"url": "http://localhost:3000/api/otp"
},
"response": []
},
{
"name": "logout",
"request": {
"auth": {
"type": "inherit",
"inherit": {}
},
"method": "POST",
"header": [],
"url": "http://localhost:3000/api/auth/logout"
},
"response": []
},
{
"name": "generate OTP",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://localhost:3000/api/otp"
},
"response": []
},
{
"name": "Fetch Mobile Number",
"request": {
"auth": {
"type": "inherit",
"inherit": {}
},
"method": "GET",
"header": [],
"url": "http://localhost:3000/api/mobile_no/by/30022497139"
},
"response": []
},
{
"name": "Get User",
"request": {
"method": "GET",
"header": [],
"url": "http://localhost:3000/api/user"
},
"response": []
},
{
"name": "Create Ticket",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"summary\":\" Login issue Related\", \r\n \"description\":\"Login related\", \r\n \r\n\t\"steps_to_reproduce\" :\"Money got debited from account\",\r\n\t\"category\": {\r\n \"name\": \"General\"\r\n\t},\r\n\t\"project\": {\r\n \"id\": 1\r\n }\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://localhost:3000/api/ticket"
},
"response": []
},
{
"name": "Add column",
"request": {
"method": "GET",
"header": [],
"url": "http://localhost:3000/api/addColumn"
},
"response": []
},
{
"name": "Dev populate",
"request": {
"method": "GET",
"header": [],
"url": "http://localhost:3000/api/dev/populate"
},
"response": []
},
{
"name": "View Ticket",
"request": {
"method": "GET",
"header": [],
"url": "http://localhost:3000/api/ticket"
},
"response": []
}
]
}

View File

@ -0,0 +1,172 @@
{
"info": {
"_postman_id": "62121509-c7ee-4d92-9684-337a1d96a6ac",
"name": "MantisBT",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "39926215"
},
"item": [
{
"name": "Create Token",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "beHEFeDe-eE0YdTIK098naY0-iirvISx",
"type": "text",
"disabled": true
},
{
"key": "Authorization",
"value": "YdTsu-wuBfc4ekVDZpfk0K4dvjmoH6Z-",
"type": "text",
"disabled": true
},
{
"key": "Authorization",
"value": "DBSC0FHDatcHiScox5vm2GPuNnNrezqM",
"type": "text"
}
],
"url": {
"raw": "http://localhost/mantisBT/api/rest/users/:user_id/token",
"protocol": "http",
"host": [
"localhost"
],
"path": [
"mantisBT",
"api",
"rest",
"users",
":user_id",
"token"
],
"variable": [
{
"key": "user_id",
"value": "5",
"type": "string"
}
]
}
},
"response": []
},
{
"name": "Create issue",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "beHEFeDe-eE0YdTIK098naY0-iirvISx",
"type": "text",
"disabled": true
},
{
"key": "Authorization",
"value": "YdTsu-wuBfc4ekVDZpfk0K4dvjmoH6Z-",
"type": "text",
"disabled": true
},
{
"key": "Authorization",
"value": "GefLBxeHyHDL9DXkGqX4qQ7Cayquqdaj",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"summary\": \"ATM Fault\",\r\n \"description\": \"ATM Fault\",\r\n \"additional_information\": \"{ATM ID : 678UCBA00988, debit_card : 600226543567}\",\r\n \"steps_to_reproduce\" :\"Money got debited from account\",\r\n \"project\": {\r\n \"id\": 1\r\n }\r\n}\r\n",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "http://localhost/mantisBT/api/rest/issues/"
},
"response": []
},
{
"name": "get a issue by issue id",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "GefLBxeHyHDL9DXkGqX4qQ7Cayquqdaj",
"type": "text"
}
],
"url": {
"raw": "http://localhost/mantisBT/api/rest/issues/:issue_id",
"protocol": "http",
"host": [
"localhost"
],
"path": [
"mantisBT",
"api",
"rest",
"issues",
":issue_id"
],
"variable": [
{
"key": "issue_id",
"value": "43"
}
]
}
},
"response": []
},
{
"name": "post note",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "DBSC0FHDatcHiScox5vm2GPuNnNrezqM",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"text\": \"test note\",\r\n \"view_state\": {\r\n \t\"name\": \"public\"\r\n }\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost/mantisBT/api/rest/issues/:issue_id/notes",
"protocol": "http",
"host": [
"localhost"
],
"path": [
"mantisBT",
"api",
"rest",
"issues",
":issue_id",
"notes"
],
"variable": [
{
"key": "issue_id",
"value": "43"
}
]
}
},
"response": []
}
]
}

124
README.md Normal file
View File

@ -0,0 +1,124 @@
# CRM Setup :
Here we have `two` modules .
- CRM Customer Module (For bank's customer)
- CRM Internal Module (For bank's internal team)
## 1. CRM Customer Module (For External Use)
### Files
`env.local`
Create a `env.local` file in the root of folder of this project.
Content of the `env.local` must be as follows:
```
SESSION_COOKIE_NAME=crm_auth
SESSION_COOKIE_PASSWORD=abcdefghijklmnopqrstuvwxyz01234567890
DB_HOST=localhost
DB_PORT=5432
DB_NAME=crm
DB_USER_NAME=
DB_USER_PASS
#CBS
ONLINE_CBS_IP=
ONLINE_CBS_PORT=
#MESSAGE
WEBSERVER_IP=
WEBSERVER_PORT=
```
### Requirement :
- Node JS (Latest)
- PostgreSQL (Latest)
### Deploy :
- Install all node module
```
npm install
```
- In the PostgreSQL create one database, the database name will be "crm"
- Build the application
```
npm run build
```
- Start the application
```
npm run start
```
- Open the following URL in the web browser for populate some data
```
http://localhost:3000/api/dev/populate
```
### Application Url :
```
http://localhost:3000/login
```
## 2. CRM Internal Module (Ticket Management System)
Here we are using "mantisBT" (open source application) for ticket management system .
### Requirement :
- Postgres server
- XAMPP 8.2.12 / Apache server
- PHP 8.2.12
### Installation & deploy :
- Create a folder into `htdocs` present under apache server.
`e.g. folder path : C:\xampp\htdocs`
`e.g. new folder : crm_internal`
- Take the CRM Internet Module (MantisBT codebase) and paste it into the folder. `e.g. crm_internal`.
- Open the folder into visual studio code .
- Change the config_inc.php as per requirement. (Present in config folder)
```
//Sample
$g_hostname = 'localhost';
$g_db_type = 'pgsql';
$g_database_name = 'kccb_ticket_tracker';
$g_db_username =
$g_db_password =
$g_db_table_prefix = 'kccb';
$g_db_table_suffix = '';
$g_default_timezone = 'Asia/Kolkata';
$g_crypto_master_salt = 'Abcdefghigklmnopqrst';
$g_path = 'http://localhost/crm_internal/'; //crm_internal is the folder name
```
- For first time installation remove the "$g_crypto_master_salt " and keep it null.
```
$g_crypto_master_salt = ""
```
- For install all the dependency and database run the below query
```
Install url :
http://localhost/crm_internal/admin/install.php
```
- After successfully install ,Login in to CRM-Internal
```
Login :
http://localhost/crm_internal/login_page.php
```
- Login Credential
- For first time user
- Username : Administrator
- password : root
### Setup:
- create a new user "kccb".
- Log in to this user `kccb`.
- Go to "My account".
- Then create a API token.
- Copy the API token.
- And paste the token to those `route.ts` file of `CRM Customer Module` where the mantisBT API is calling.
- create development team's user as protected.
- create a project, and keep the name as "KCCB".

6
TODO.md Normal file
View File

@ -0,0 +1,6 @@
# Todo
- Fix scroll issues with appshell main area
- Display Normal error inside the dialog box
- Give search box for fetching the user
- Apply the delete functionality

9
next.config.mjs Normal file
View File

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

7515
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "CRM",
"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",
"crm": "file:",
"CRM": "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": "^7.0.6",
"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",
"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",
"postcss": "^8.4.38",
"postcss-preset-mantine": "^1.15.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5"
}
}

14
postcss.config.cjs Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,46 @@
"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) {
console.log(error);
notifications.show({
color: 'red',
title: error.code,
message: error.message
})
}
return user;
}
const UserContext = React.createContext<User | null | undefined>(undefined);
function UserContextProvider({ children }: { children: React.ReactNode }) {
const userQuery = useQuery({
queryKey: ['user'],
queryFn: queryUser,
networkMode: 'always',
});
return (
<UserContext.Provider value={userQuery.data}>
{children}
</UserContext.Provider>
)
}
const UserContextConsumer = UserContext.Consumer;
export { UserContext, UserContextProvider, UserContextConsumer }

View File

@ -0,0 +1,33 @@
import { Entity, Column, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"
@Entity({ name: 'ticket' })
export class Ticket {
@PrimaryGeneratedColumn()
// @PrimaryColumn("int")
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('varchar')
created_date! : string
@Column({type: 'varchar',nullable:true})
assign_by! :string
}

View File

@ -0,0 +1,27 @@
import { Entity, Column, PrimaryColumn } from "typeorm"
@Entity({ name: 'user' })
export class User {
@PrimaryColumn("int")
id!: number
@Column("bigint")
bank_account_no!: number
@Column("varchar")
title!: string
@Column("varchar")
first_name!: string
@Column("varchar")
middle_name!: string
@Column("varchar")
last_name!: string
@Column("bigint",{nullable:true})
mobile_number!: number
}

View File

@ -0,0 +1,3 @@
id,bank_account_no,title,first_name,middle_name,last_name,mobile_number
6000,30022497139,Mr.,Rajat,Kumar,Maharana,7890544527
6001,30022497138,Ms.,Tomosa,,Sarkar,7890544527
1 id bank_account_no title first_name middle_name last_name mobile_number
2 6000 30022497139 Mr. Rajat Kumar Maharana 7890544527
3 6001 30022497138 Ms. Tomosa Sarkar 7890544527

View File

@ -0,0 +1,45 @@
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,
//entities: [User,Ticket],
subscribers: [],
migrations: []
})
static async getConnection(): Promise<DataSource> {
if (!this.dataSource.isInitialized)
await this.dataSource.initialize()
return this.dataSource;
}
static async getMantisConnection(): Promise<DataSource> {
if (!this.dataSource_MantisBT.isInitialized)
await this.dataSource_MantisBT.initialize()
return this.dataSource_MantisBT;
}
}
export default AppDataSource;

View File

@ -0,0 +1,25 @@
"use client";
import { MantineColorsTuple, createTheme } from "@mantine/core";
const KccbColors: MantineColorsTuple = [
"#eafded",
"#d7f8dd",
"#adefba",
"#80e792",
"#5bdf73",
"#44db5d",
"#37d952",
"#29c142",
"#1fab39",
"#08942d"
];
export const KccbTheme = createTheme({
/* Put your mantine theme override here */
primaryColor: 'kccb-colors',
colors: {
'kccb-colors': KccbColors
}
// primaryColor: 'kccb-colors'
});

View File

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

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

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

View File

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

View File

@ -0,0 +1,27 @@
import AppDataSource from '@/app/_data/source/app-data-source'
import { TableColumn } from 'typeorm';
export async function GET(request: Request) {
try {
const connection = await AppDataSource.getMantisConnection();
//const userRepo = connection.getRepository(User);
const queryRunner =connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.addColumn("kccb_bug_text",new TableColumn(
{
name : "information",
type: "jsonb",
isNullable: true,
}
))
return Response.json("column added.");
}
catch (error) {
console.error(error);
return Response.json(null, { status: 500 })
}
}

View File

@ -0,0 +1,42 @@
import AppDataSource from '@/app/_data/source/app-data-source'
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';
import SessionPayload from '../../session/payload';
import { User } from '@/app/_data/entities/User';
import getSessionOptions from '../../session/options';
export async function POST(req: Request) {
try {
const session = await getIronSession<SessionPayload>(cookies(), getSessionOptions())
const body = await req.json();
const { AccNo } = body;
console.log(AccNo)
if (!AccNo || AccNo === '') {
return Response.json({ message: "Account No can't be empty" })
}
const connection = await AppDataSource.getConnection();
const userRepo = connection.getRepository(User);
const user = await userRepo.createQueryBuilder('user')
.select(['id', 'bank_account_no'])
.where("user.bank_account_no = :AccountNo", { AccountNo: AccNo })
.getRawOne();
if (!user)
return Response.json({ message: 'Please Enter Valid Account No Or contact to administrator.' })
session.accountNo = user.bank_account_no;
session.TwoStepAuthentication = false;
session.otp = NaN;
session.expiryTime = NaN;
await session.save();
//console.log(session);
return Response.json({ ok: true })
} catch (error) {
console.error(error);
return Response.json(null, { status: 500 })
}
}

View File

@ -0,0 +1,21 @@
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import getSessionOptions from "../session/options";
import SessionPayload from "../session/payload";
export async function POST(request: Request) {
try {
const session = await getIronSession<SessionPayload>(cookies(), getSessionOptions());
if (!session) return Response.json(null, { status: 404 });
session.destroy();
return Response.json({message: 'logout successfully'});
} catch (error) {
console.error(error);
return Response.json(null, { status: 500 })
}
}

View File

@ -0,0 +1,18 @@
import { SessionOptions } from "iron-session";
export default function getSessionOptions(): SessionOptions {
const sessionOptions: SessionOptions = {
cookieName: process.env.SESSION_COOKIE_NAME ?? "",
password: process.env.SESSION_COOKIE_PASSWORD ?? "",
cookieOptions: {
// httpOnly: true,
secure: false, // false in local (non-HTTPS) development | true in production (if HTTPS)
// sameSite: "lax",// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax
// maxAge: (ttl === 0 ? 2147483647 : ttl) - 60, // Expire cookie before the session expires.
// path: "/",
}
}
return sessionOptions;
}

View File

@ -0,0 +1,11 @@
import AccountNo from "@/app/_types/accountNo"
type SessionPayload = {
accountNo: AccountNo,
TwoStepAuthentication :boolean,
otp : number,
expiryTime :number
}
export default SessionPayload

2
src/app/api/auth/util.ts Normal file
View File

@ -0,0 +1,2 @@
const saltAndHashedPassword_regex = /^([0-9a-f]{32}:[0-9a-f]{128})$/;
export const isValidSaltAndHashedPassword = (s: string) => saltAndHashedPassword_regex.test(s);

View File

@ -0,0 +1,38 @@
import { User } from "@/app/_data/entities/User";
import { parse } from "csv-parse";
import { createReadStream } from "fs";
import AppDataSource from "@/app/_data/source/app-data-source";
export async function GET(request: Request) {
if (!['development', 'test'].includes(process.env.NODE_ENV))
return Response.json(null, { status: 404 });
try {
// Create User
const connection = await AppDataSource.getConnection();
const userRepo = connection.getRepository(User);
const csvReadStream = createReadStream("src/app/_data/samples/users.csv");
let parser = parse({ columns: true });
csvReadStream.pipe(parser);
userRepo.delete({})
for await (const user of parser) {
const newUser = new User();
newUser.id = user.id
newUser.bank_account_no=user.bank_account_no
newUser.title=user.title
newUser.first_name=user.first_name
newUser.middle_name=user.middle_name
newUser.last_name=user.last_name
newUser.mobile_number=user.mobile_number
await userRepo.save(newUser);
}
return Response.json({ done: true });
} catch (error) {
console.error(error);
return Response.json("Failed", { status: 500 });
}
}

View File

@ -0,0 +1,36 @@
import AppDataSource from '@/app/_data/source/app-data-source'
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';
import SessionPayload from '@/app/api/auth/session/payload';
import { User } from '@/app/_data/entities/User';
import getSessionOptions from '@/app/api/auth/session/options';
export async function GET(request: Request , { params }: { params: { account_no: number}}) {
try {
const { account_no } = params;
const session = await getIronSession<SessionPayload>(cookies(), getSessionOptions())
const connection = await AppDataSource.getConnection();
const userRepo = connection.getRepository(User);
const user_mob_no = await userRepo.createQueryBuilder('user')
.select(['mobile_number'])
.where("user.bank_account_no = :AccountNo", { AccountNo: account_no })
.getRawOne();
if (user_mob_no.mobile_number == null)
return Response.json({ message: "Mobile number is not updated.Please contact with administrator" })
console.log(user_mob_no.mobile_number)
//session.otp = NaN;
//session.mobileNo = user_mob_no.mobile_number;
await session.save();
console.log(session);
return Response.json(user_mob_no)
}
catch (error) {
console.error(error);
return Response.json(null, { status: 500 })
}
}

View File

@ -0,0 +1,9 @@
export function generateOTP(length: number) {
const digits = '0123456789';
let otp = '';
otp += digits[Math.floor(Math.random() * 9)+1]; //first digit cannot be zero
for (let i = 1; i < length; i++) {
otp += digits[Math.floor(Math.random() * digits.length)];
}
return otp;
}

88
src/app/api/otp/route.ts Normal file
View File

@ -0,0 +1,88 @@
import AppDataSource from "@/app/_data/source/app-data-source";
import { getIronSession } from "iron-session";
import SessionPayload from "../auth/session/payload";
import { cookies } from "next/headers";
import getSessionOptions from "../auth/session/options";
import { User } from "@/app/_data/entities/User";
import { generateOTP } from "./generate_otp";
export async function GET(req: Request) {
try {
const session = await getIronSession<SessionPayload>(cookies(), getSessionOptions())
const connection = await AppDataSource.getConnection();
const userRepo = connection.getRepository(User);
if(!session.accountNo){
return Response.json({ message:`Please sign in before get OTP`})
}
const user_mob_no = await userRepo.createQueryBuilder('user')
.select(['mobile_number'])
.where("user.bank_account_no = :AccountNo", { AccountNo: session.accountNo})
.getRawOne();
const mobileNumber = user_mob_no.mobile_number
if(mobileNumber === null )
return Response.json({ message: "Mobile number is not updated.Please contact with administrator"},{status:404})
// const otp = generateOTP(5)
const otp = "199812"; //For demo
session.otp= parseInt(otp);
session.expiryTime=Date.now() + 90 * 1000;
await session.save();
//console.log(session);
const maskedPartMobNo= 'x'.repeat(mobileNumber.toString().length -3);
const lastThreeDigit= mobileNumber.toString().slice(-3);
console.log( "OTP :" , otp);
return Response.json({ message:`New OTP has been sent to your register mobile number ${maskedPartMobNo + lastThreeDigit}-KCCB`})
}
catch (error) {
console.error(error);
return Response.json(null, { status: 500 })
}
}
export async function POST(req: Request) {
try {
const session = await getIronSession<SessionPayload>(cookies(), getSessionOptions())
const body = await req.json();
const { OTP} = body;
if(!session.accountNo){
return Response.json({ message:`Please sign in before get OTP`})
}
if (!OTP || OTP === '' || Number.isNaN(OTP))
return Response.json({ message: "OTP field can not be blank" })
console.log(session)
// Check for OTP expiry time
if(Date.now()> session.expiryTime){
session.otp =NaN;
session.expiryTime=NaN;
await session.save();
}
//console.log(session);
if(Number.isNaN(session.otp))
return Response.json({ error: "The OTP Session has timed out "},{status:401})
if(session.otp !== parseInt(OTP))
return Response.json({ error: "Please enter valid OTP."},{status:401})
session.TwoStepAuthentication = true;
await session.save();
//console.log(session);
return Response.json({ ok: true })
}
catch (error)
{
console.error(error);
return Response.json(null, { status: 500 })
}
}

156
src/app/api/ticket/route.ts Normal file
View File

@ -0,0 +1,156 @@
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import getSessionOptions from "../auth/session/options";
import SessionPayload from "../auth/session/payload";
import AppDataSource from "@/app/_data/source/app-data-source";
import { Ticket } from "@/app/_data/entities/Ticket";
import { User } from "@/app/_data/entities/User";
export async function POST(request: Request) {
try {
const session = await getIronSession<SessionPayload>(
cookies(),
getSessionOptions()
);
if (!session.accountNo) {
return Response.json({ message: `Please sign in before raise a ticket` })
}
if (!session.TwoStepAuthentication) {
return Response.json(null);
}
//console.log(new Date().toLocaleString("en-IN"));
const URL = "http://localhost/crm_internal/api/rest/issues/";
const body = await request.json();
//request_category, nature_of_request, issue, message
const { summary, description, additional_information, steps_to_reproduce } = body;
const project = { id: 1 };
const requestBody = { ...body, project };
const result = await (await fetch(URL, {
method: "POST",
headers:
{ "Content-Type": "application/json", "Authorization": "M7xHFMiZcg_oPrGZoRY-kPoivoniK3BO" },
body: JSON.stringify(requestBody)
}))
if (result.status === 403) {
return Response.json({ message: 'Invalid Token' }, { status: 403 })
}
if (result.status === 400) {
return Response.json({ message: 'Project not specified' }, { status: 400 })
}
if (result.status === 404) {
return Response.json({ message: 'Project not found' }, { status: 404 })
}
if (result.status === 429) {
return Response.json({ message: 'Please raised ticket after sometime.' }, { status: 429 })
}
///CRM database management
const connection_crm = await AppDataSource.getConnection();
const userRepo = connection_crm.getRepository(User);
const user_table = await userRepo.createQueryBuilder('user')
.select(['first_name', 'middle_name', 'last_name'])
.where("user.bank_account_no = :AccountNo", { AccountNo: session.accountNo })
.getRawOne();
const user_name = (user_table.first_name ? user_table.first_name + ' ' : '') +
(user_table.middle_name ? user_table.middle_name + ' ' : '') +
(user_table.last_name ? user_table.last_name + ' ' : '');
const ticketRepo = connection_crm.getRepository(Ticket);
const ticket = new Ticket();
ticket.category_of_request = summary;
ticket.nature_of_request = description;
ticket.additional_info = additional_information
ticket.message = steps_to_reproduce;
ticket.created_by = user_name;
//ticket.created_date = new Date().toString();
ticket.created_date=new Date().toLocaleString("en-IN");
await ticketRepo.save(ticket);
return Response.json({ message: `Ticket No:${result.statusText} created successfully` })
}
catch (error) {
console.error(error);
return Response.json(null, { status: 500 });
}
}
export async function GET(request: Request) {
try {
const session = await getIronSession<SessionPayload>(cookies(), getSessionOptions());
if (!session.TwoStepAuthentication) {
return Response.json(null);
}
if (!session.accountNo) {
return Response.json({ message: `Please sign in before raise a ticket` })
}
const connection_crm = await AppDataSource.getConnection();
const connection_mantis = await AppDataSource.getMantisConnection();
const userRepo = connection_crm.getRepository(User);
const user_table = await userRepo.createQueryBuilder('user')
.select(['first_name', 'middle_name', 'last_name'])
.where("user.bank_account_no = :AccountNo", { AccountNo: session.accountNo })
.getRawOne();
const user_name = (user_table.first_name ? user_table.first_name + ' ' : '') +
(user_table.middle_name ? user_table.middle_name + ' ' : '') +
(user_table.last_name ? user_table.last_name + ' ' : '');
const ticketRepo = connection_crm.getRepository(Ticket);
const ticket_list = await ticketRepo.createQueryBuilder('ticket')
.select(['id', 'category_of_request', 'nature_of_request', 'created_date','additional_info'])
.where("ticket.created_by = :created_by", { created_by: user_name }).getRawMany();
if(!ticket_list){return Response.json('No Details Found')}
const ticket_ids = ticket_list.map(ticket => ticket.id);
// Mantis-database fetching for "status" & "message"
const queryRunner = connection_mantis.createQueryRunner();
await queryRunner.connect();
let ticketResolution: { [key: number]: string } = {};
let ticketMessage: { [key: number]: string[] } = {};
for (const id of ticket_ids) {
// For status of ticket
const ticket_resolution = await queryRunner.query('SELECT resolution,handler_id FROM "kccb_bug" WHERE id = $1', [id]);
const ticket_status =ticket_resolution[0]?.resolution
const ticket_handler =ticket_resolution[0]?.handler_id
let status :string ='';
if(ticket_status === 10 && ticket_handler === 0){status = 'Open'}
if(ticket_handler > 0){status = 'In Progress'}
if(ticket_status === 20){status='Resolved'}
if(ticket_status === 30){status='Reopen'}
ticketResolution[id] = status
// For Message of ticket
const ticket_note = await queryRunner.query('SELECT id FROM "kccb_bugnote" WHERE bug_id = $1', [id]);
const ticket_note_ids =ticket_note.map((item:{id:number}) =>item.id)
for(const note_id of ticket_note_ids ){
const ticket_note_text = await queryRunner.query('SELECT note FROM "kccb_bugnote_text" WHERE id = $1', [note_id]);
if (!ticketMessage[id]) {
ticketMessage[id] = [];
}
ticketMessage[id].push(...ticket_note_text);
}
}
const result = ticket_list.map(item =>
(
{
...item,status:ticketResolution[item.id]||null,message: ticketMessage[item.id]||null
}
)
)
queryRunner.release();
return Response.json(result)
}
catch (error) {
console.error(error);
return Response.json(null, { status: 500 });
}
}

39
src/app/api/user/route.ts Normal file
View File

@ -0,0 +1,39 @@
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import getSessionOptions from "../auth/session/options";
import SessionPayload from "../auth/session/payload";
import AppDataSource from "@/app/_data/source/app-data-source";
import { User } from "@/app/_data/entities/User";
export async function GET(request: Request) {
try {
const session = await getIronSession<SessionPayload>(
cookies(),
getSessionOptions()
);
if (!session.accountNo) {
return Response.json(null);
}
const connection = await AppDataSource.getConnection();
const userRepo = connection.getRepository(User);
let user_details = await userRepo
.createQueryBuilder("user")
.select(["id","bank_account_no"])
.where("user.bank_account_no = :userAccountNo", { userAccountNo: session.accountNo })
.getRawOne();
//console.log(session)
// if (!user_details || session.TwoStepAuthentication === false)
if (!user_details)
return Response.json({ message: "User_details not found" }, { status: 404 });
return Response.json(user_details);
} catch (error) {
console.error(error);
return Response.json(null, { status: 500 });
}
}

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

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

View File

@ -0,0 +1,80 @@
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family:
Greycliff CF,
var(--mantine-font-family);
}
.navbar {
flex-grow: 1;
padding: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
}
.navbarMain {
flex: 1;
}
.header {
padding-bottom: var(--mantine-spacing-md);
margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.user {
padding: var(--mantine-spacing-md);
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
border-radius: var(--mantine-radius-sm);
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
}
}
.footer {
padding-top: var(--mantine-spacing-md);
margin-top: var(--mantine-spacing-md);
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.link {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
.linkIcon {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
}
&[data-active] {
&,
&:hover {
background-color: var(--mantine-primary-color-light);
color: var(--mantine-primary-color-light-color);
.linkIcon {
color: var(--mantine-primary-color-light-color);
}
}
}
}
.linkIcon {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
margin-right: var(--mantine-spacing-sm);
width: rem(25px);
height: rem(25px);
}

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

@ -0,0 +1,160 @@
"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) {
console.log(error);
notifications.show({
color: 'red',
title: error.code,
message: error.message
})
}
}
export default function HomeLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const user = useContext(UserContext);
if (user === null) redirect('/login');
const router = useRouter();
const queryClient = useQueryClient();
const [burgerOpened, { toggle: toggleBurger }] = useDisclosure();
const [activeNavItem, setActiveNavItem] = useState<string | null>(null);
const logoutMutation = useMutation({
mutationKey: ['logout'],
mutationFn: logout
})
async function handleLogout(event: React.MouseEvent) {
event.preventDefault();
await logoutMutation.mutateAsync();
await queryClient.refetchQueries({ queryKey: ['user'] })
}
let navItems: React.ReactNode[] = [];
navItems.push(<>
<NavLink key={'raised-ticket'} label={
<>
<IconTicket size={20} style={{ marginRight: '5px', transform: 'translateY(5px)' }} />
<span style={{ fontSize: '18px' }}>Create Ticket </span>
</>
}
active={activeNavItem === 'raised-ticket' || undefined}
onClick={event => {
event.preventDefault();
setActiveNavItem('raised-ticket')
router.push('/home/user/raised-ticket')
}}
/>
<NavLink key={'view-ticket'} label={<><IconEye size={20} style={{ marginRight: '8px', transform: 'translateY(5px)' }} />
<span style={{ fontSize: '18px' }}>View Ticket </span>
</>
}
active={activeNavItem === 'view-ticket' || undefined}
onClick={event => {
event.preventDefault();
setActiveNavItem('view-ticket')
router.push('/home/user/view-ticket');
}}
/>
</>
)
return (
<>
<LoadingOverlay
visible={!user || logoutMutation.isPending || logoutMutation.isSuccess}
overlayProps={{ backgroundOpacity: 1 }} />
{user && <AppShell
header={{ height: { base: 60, md: 70, lg: 80 } }}
navbar={{
width: { base: 200, md: 250, lg: 300 },
breakpoint: 'sm',
collapsed: { mobile: !burgerOpened },
}}
padding="lg"
>
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Group>
<Burger opened={burgerOpened} onClick={toggleBurger} hiddenFrom="sm" size="sm" />
<Image
radius='md'
h={50}
w='auto'
fit='contain'
src={'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-ZljkQXsXUP1-EwnIPZtVq_JWhwGUW7b0_eSMno-bag&s'}
/>
<Title order={2} className={classes.title}>Customer Request Management</Title>
</Group>
{/* <Group>
<UnstyledButton className={classes.user} onClick={() => {
setActiveNavItem(null);
router.push('/home/user');
}}>
</UnstyledButton>
</Group> */}
<Group>
<Avatar radius="xl" src={null} alt={user.bank_account_no} color={KccbTheme.primaryColor}>
</Avatar>
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
Account No: [{user.bank_account_no}]
</Text>
{/* <Text c='dimmed' size="sm" component="span">User: </Text>
<Code c='dimmed'>{user.id}</Code> */}
</div>
</Group>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<Text>Menu</Text>
<nav className={classes.navbar}>
<div className={classes.navbarMain}>
<ScrollArea>
{navItems}
</ScrollArea>
</div>
</nav>
<div className={classes.footer}>
<a className={classes.link} onClick={handleLogout}>
<IconLogout className={classes.linkIcon} stroke={1.5} />
<Text>Logout</Text>
</a>
</div>
</AppShell.Navbar>
<AppShell.Main>
{children}
</AppShell.Main>
</AppShell>}
</>
)
}

View File

@ -0,0 +1,80 @@
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family:
Greycliff CF,
var(--mantine-font-family);
}
.navbar {
flex-grow: 1;
padding: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
}
.navbarMain {
flex: 1;
}
.header {
padding-bottom: var(--mantine-spacing-md);
margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.user {
padding: var(--mantine-spacing-md);
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
border-radius: var(--mantine-radius-sm);
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
}
}
.footer {
padding-top: var(--mantine-spacing-md);
margin-top: var(--mantine-spacing-md);
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.link {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
.linkIcon {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
}
&[data-active] {
&,
&:hover {
background-color: var(--mantine-primary-color-light);
color: var(--mantine-primary-color-light-color);
.linkIcon {
color: var(--mantine-primary-color-light-color);
}
}
}
}
.linkIcon {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
margin-right: var(--mantine-spacing-sm);
width: rem(25px);
height: rem(25px);
}

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

@ -0,0 +1,15 @@
"use client";
import { UserContextConsumer } from "../_components/user-context";
export default function Home() {
return (
<>
<p>Welcome to CRM Portal</p>
<p>For Issue a ticket ,click on create ticket</p>
<UserContextConsumer>
{user => user && <p>Account No: {user.bank_account_no}</p>}
</UserContextConsumer>
</>
);
}

View File

@ -0,0 +1,265 @@
"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: description,
description: description,
additional_information: additionalInformation.join(", "),
steps_to_reproduce: message,
};
try {
const response = await axios.post("/api/ticket", requestBody);
const data = await response.data;
console.log('API 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">ATM Related</option>
<option value="IB">Internet Banking</option>
<option value="MB">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" && <option value="Transaction Issue">Transaction Issue</option>}
{category === "UPI" && <option value="UPI Issue">UPI Issue</option>}
{category === "IB" && (
<>
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
<option value="Fund Transfer failure">Fund Transfer Failure</option>
<option value="Network Issue">Network Issue</option>
</>
)}
{category === "MB" && (
<>
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
<option value="Fund Transfer failure">Fund Transfer Failure</option>
<option value="Network Issue">Network Issue</option>
</>
)}
{category === "Others" && (
<>
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
<option value="Others">Others</option>
</>
)}
</select>
</label>
{additionalFields.map((field, index) => (
<div key={index}>
{field === "Transaction Date" ? (
<label>
{field}: <span style={{ color: "red" }}>*</span>
<input
type="date"
value={additionalFieldValues[field] || ""}
onChange={(e) => handleAdditionalFieldChange(field, e.target.value)}
required
style={{
display: "block",
marginBottom: "10px",
width: "100%",
}}
/>
</label>
) : (
<label>
{field}: <span style={{ color: "red" }}>*</span>
<input
type="text"
placeholder={`Enter ${field}`}
value={additionalFieldValues[field] || ""}
onChange={(e) => handleAdditionalFieldChange(field, e.target.value)}
required
style={{
display: "block",
marginBottom: "10px",
width: "100%",
}}
/>
</label>
)}
</div>
))}
<label>
Message (Max 500 characters) <span style={{ color: "red" }}>*</span>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
maxLength={500}
rows={5}
required
style={{
display: "block",
marginBottom: "10px",
width: "100%",
resize: "none",
}}
/>
</label>
<button
type="submit"
style={{
display: "block",
width: "100%",
padding: "10px",
backgroundColor: "#4CAF50",
color: "white",
border: "none",
cursor: "pointer",
}}
>
Submit
</button>
</form>
</div>
);
};
export default ComplaintForm;

View File

@ -0,0 +1,178 @@
"use client";
import {
ActionIcon,
Button,
Container,
Flex,
Group,
Modal,
Space,
Table,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconEye, IconMessage, IconSearch } from "@tabler/icons-react";
import axios from "axios";
import { useEffect, useState } from "react";
interface Message {
note: string;
}
interface Ticket {
id: string;
category_of_request: string;
nature_of_request: string;
created_date: string;
status: string;
message: Message[];
}
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);
// 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.id) - parseInt(a.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();
}, []);
const handleOpenMessage = (messages: Message[]) => {
setActiveMessages(messages);
open();
};
//Add conditional row styles
const getRowStyle = (status: string) => {
switch (status.toLowerCase()) {
case "resolved":
return { backgroundColor: "#e0e3df", color: "#000"}; // Grey for closed
default:
return {};
}
};
const rows = filteredTickets.map((ticket) => (
<Table.Tr key={ticket.id} style={getRowStyle(ticket.status)}>
<Table.Td>{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
variant="subtle"
onClick={() => handleOpenMessage(ticket.message)}
>
<Tooltip label="Message">
<IconMessage/>
</Tooltip>
</ActionIcon>
</Group>
</Table.Td>
<Table.Td>
<Group>
<ActionIcon variant="subtle">
<Tooltip label="Details of the Ticket">
<IconEye/>
</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>Message</Table.Th>
<Table.Th>Summary</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
<Modal opened={opened} onClose={close} title="Message">
<Flex direction="column" gap="sm">
{activeMessages?.map((msg, index) => (
<div
key={index}
style={{
padding: "0.5rem",
background: index % 2 === 0 ? "#f1f3f5" : "#dce6f2",
borderRadius: "8px",
maxWidth: "70%",
}}
>
{msg.note}
</div>
))}
<Button onClick={close} mt="sm">
Close
</Button>
</Flex>
</Modal>
</Container>
);
}

View File

@ -0,0 +1,159 @@
"use client";
import {
ActionIcon,
Button,
Container,
Flex,
Group,
Modal,
Space,
Table,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconMessage, IconSearch } from "@tabler/icons-react";
import { useEffect, useState } from "react";
interface Message {
note: string;
}
interface Ticket {
id: string;
category_of_request: string;
nature_of_request: string;
created_date: string;
status: string;
message: Message[];
}
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 [filteredUsers, setFilteredUsers] = useState<Ticket[]>([]);
const [activeMessages, setActiveMessages] = useState<Message[] | null>(null);
// Fetch tickets from the API
const fetchTickets = async () => {
try {
const response = await fetch("/api/ticket");
if (!response.ok) {
throw new Error("Failed to fetch tickets");
}
const data: Ticket[] = await response.json();
setTickets(data);
setFilteredTickets(data); // Set initial filtered tickets
} catch (error) {
console.error("Error fetching tickets:", error);
}
};
useEffect(() => {
fetchTickets(); // Call API when component mounts
}, []);
// Filter tickets based on the search query
useEffect(() => {
const results = tickets.filter((ticket) =>
ticket.category_of_request.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredTickets(results);
}, [searchQuery, tickets]);
const handleOpenMessage = (messages: Message[]) => {
setActiveMessages(messages);
open();
};
const rows = filteredUsers.map((ticket) => (
<Table.Tr key={ticket.id}>
<Table.Td>{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
variant="subtle"
onClick={() => handleOpenMessage(ticket.message)}
>
<Tooltip label="Message">
<IconMessage />
</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"
>
<thead>
<tr>
<th>Ticket ID</th>
<th>Category</th>
<th>Description</th>
<th>Timestamp</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
<Modal opened={opened} onClose={close} title="Message">
<Flex direction="column" gap="sm">
{activeMessages?.map((msg, index) => (
<div
key={index}
style={{
padding: "0.5rem",
background: index % 2 === 0 ? "#f1f3f5" : "#dce6f2",
borderRadius: "8px",
maxWidth: "70%",
}}
>
{msg.note}
</div>
))}
<Button onClick={close} mt="sm">
Close
</Button>
</Flex>
</Modal>
</Container>
);
}

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

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

@ -0,0 +1,185 @@
"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);
} catch (error: AxiosError | any) {
console.log(error);
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) {
console.log(error);
notifications.show({
color: 'red',
title: error.response.status,
message: error.response.data.error
})
}
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"
{...register('OTP', { required: true })} />
{errors.OTP && <Text c='red'>*Required</Text>}
<Center mt="lg">
<Text >{timerActive ? `Resend OTP in ${countdown}s` : 'Resend OTP Available'} </Text>
</Center>
<Group justify="center">
<Button mt="xl" size="md" type='submit' >
Validate OTP
</Button>
{validateOTPMutation.data?.message && <Text c='red' ta='center' pt={30}>{validateOTPMutation.data.message}</Text>}
<Button mt="xl" size="md" type='submit' color='blue' onClick={handleResend} disabled={timerActive} >
Resend OTP
</Button>
{/* {resendOTPMutation?.data?.message && <Text c='red' ta='center' pt={30}>{resendOTPMutation.data.message}</Text>} */}
</Group>
</Paper>
</div>
<Image src={image.src} className={classes.desktopImage} />
</SimpleGrid>
<footer justify-content='center' color='green'>Copyright © 2025 Tata Consultancy Services, All rights reserved.</footer>
</Container>
);
}

View File

@ -0,0 +1,29 @@
.root {
width: 100vw;
height: 100vh;
}
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family:
Greycliff CF,
var(--mantine-font-family);
}
.mobileImage {
@media (min-width: 48em) {
display: none;
}
}
.desktopImage {
object-fit: cover;
width: 100%;
height: 100%;
@media (max-width: 47.99em) {
display: none;
}
}

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

@ -0,0 +1,95 @@
"use client"
import classes from './page.module.css';
import React, { useContext, useState } from "react";
import { TextInput, Button, Container, Title, Image, SimpleGrid, LoadingOverlay, Avatar, Paper, Text } from "@mantine/core";
import image from './helpdesk.png';
import { useForm, SubmitHandler } from "react-hook-form";
import axios, { AxiosError } from "axios";
import { notifications, showNotification } from "@mantine/notifications";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useDisclosure } from "@mantine/hooks";
type SendAccNoInput = {
AccNo: string
}
type Result = {
ok?: boolean
message?: string
}
async function handleRequestOTP(SendAccNoInput: SendAccNoInput) {
let Result: Result = { ok: false }
try {
const response = await axios.post("/api/auth/login/account_no", SendAccNoInput);
if (response.data.ok) {
const otp_response = await axios.get("/api/otp");
if (otp_response.status === 200) {
// redirect('/otp');
window.location.href = 'login/otp'
}
}
Object.assign(Result, response.data);
} catch (error: AxiosError | any) {
console.log(error);
notifications.show({
color: 'red',
title: error.response.status,
message: error.response.data.message
})
}
return Result;
}
export default function sendOTP() {
const queryClient = useQueryClient();
const { register, handleSubmit, formState: { errors } } = useForm<SendAccNoInput>()
const sendOTPMutation = useMutation<Result, AxiosError, SendAccNoInput>({
mutationKey: ['sendOtp'],
mutationFn: handleRequestOTP,
})
const onSubmit: SubmitHandler<SendAccNoInput> = async (SendAccNoInput) => {
const Result = await sendOTPMutation.mutateAsync(SendAccNoInput);
if (Result.ok)
await queryClient.refetchQueries({ queryKey: ['OTP'] });
}
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={sendOTPMutation.isPending || sendOTPMutation.data?.ok} />
<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="Customer Account Number *"
placeholder="e.g. 1234"
size="md"
{...register('AccNo', { required: true })} />
{errors.AccNo && <Text c='red'>*Required</Text>}
<Button fullWidth mt="xl" size="md" type='submit' >
Send OTP
</Button>
{sendOTPMutation.data?.message &&
<Text c='red' ta='center' pt={30}>{sendOTPMutation.data.message}</Text>}
</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>
);
}

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

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

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

@ -0,0 +1,22 @@
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserContextProvider } from './_components/user-context';
import { MantineProvider } from '@mantine/core';
import { KccbTheme } from './_themes/KccbTheme';
import { Notifications } from '@mantine/notifications';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<MantineProvider theme={KccbTheme} defaultColorScheme='light'>
<Notifications position='bottom-left' />
<QueryClientProvider client={queryClient}>
<UserContextProvider>
{children}
</UserContextProvider>
</QueryClientProvider>
</MantineProvider>
)
}

16
test.js Normal file
View File

@ -0,0 +1,16 @@
function getRandom() {
return Math.random()
}
class Test {
static random = getRandom();
static getRandom() {
return Test.random;
}
}
console.log(Test.getRandom())
console.log(Test.getRandom())
console.log(Test.getRandom())

41
tsconfig.json Normal file
View File

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