first commit-CRM_module
This commit is contained in:
commit
c8b26fa46b
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal 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
110
API.md
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
160
Docs/CRM.postman_collection.json
Normal file
160
Docs/CRM.postman_collection.json
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
172
Docs/MantisBT.postman_collection.json
Normal file
172
Docs/MantisBT.postman_collection.json
Normal 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
124
README.md
Normal 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
6
TODO.md
Normal 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
9
next.config.mjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
serverComponentsExternalPackages: ["typeorm", "knex"],
|
||||||
|
},
|
||||||
|
reactStrictMode: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
7515
package-lock.json
generated
Normal file
7515
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
package.json
Normal file
60
package.json
Normal 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
14
postcss.config.cjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-preset-mantine": {},
|
||||||
|
"postcss-simple-vars": {
|
||||||
|
variables: {
|
||||||
|
"mantine-breakpoint-xs": "36em",
|
||||||
|
"mantine-breakpoint-sm": "48em",
|
||||||
|
"mantine-breakpoint-md": "62em",
|
||||||
|
"mantine-breakpoint-lg": "75em",
|
||||||
|
"mantine-breakpoint-xl": "88em",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
33
src/app/_components/error/http-error.module.css
Normal file
33
src/app/_components/error/http-error.module.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
.label {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: rem(220px);
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: calc(var(--mantine-spacing-xl) * 1.5);
|
||||||
|
|
||||||
|
@media (max-width: $mantine-breakpoint-sm) {
|
||||||
|
font-size: rem(120px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family:
|
||||||
|
Greycliff CF,
|
||||||
|
var(--mantine-font-family);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: rem(38px);
|
||||||
|
color: var(--mantine-color-white);
|
||||||
|
|
||||||
|
@media (max-width: $mantine-breakpoint-sm) {
|
||||||
|
font-size: rem(32px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
max-width: rem(540px);
|
||||||
|
margin: auto;
|
||||||
|
margin-top: var(--mantine-spacing-xl);
|
||||||
|
margin-bottom: calc(var(--mantine-spacing-xl) * 1.5);
|
||||||
|
color: var(--mantine-color-blue-1);
|
||||||
|
}
|
14
src/app/_components/error/http-error.tsx
Normal file
14
src/app/_components/error/http-error.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Title, Text, Button, Container, Group } from '@mantine/core';
|
||||||
|
import classes from './http-error.module.css';
|
||||||
|
|
||||||
|
export function HttpError(props: { status: number, code: string, message: string }) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div className={classes.label}>{props.status}</div>
|
||||||
|
<Title className={classes.title}>{props.code}</Title>
|
||||||
|
<Text size="lg" ta="center" className={classes.description}>
|
||||||
|
{props.message}
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
52
src/app/_components/success-component/SuccessMessage.tsx
Normal file
52
src/app/_components/success-component/SuccessMessage.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Center, ActionIcon, Text } from "@mantine/core";
|
||||||
|
import { IconCircleDashedCheck, IconX } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface SuccessMessageProps {
|
||||||
|
username: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SuccessMessage({
|
||||||
|
username,
|
||||||
|
onClose,
|
||||||
|
}: SuccessMessageProps) {
|
||||||
|
return (
|
||||||
|
<Center style={{ height: "400px" }}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: "375px",
|
||||||
|
height: "375px",
|
||||||
|
backgroundColor: "#f0f0f0",
|
||||||
|
borderRadius: "8px",
|
||||||
|
position: "relative",
|
||||||
|
boxShadow: "0px 4px 10px rgba(0,0,0,0.1)",
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "10px",
|
||||||
|
left: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={24} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Center
|
||||||
|
style={{
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCircleDashedCheck size={100} color="green" />
|
||||||
|
<Text mt="lg" size="lg" style={{ fontWeight: 500, align: "center" }}>
|
||||||
|
New User <b>{username}</b> created Successfully
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
46
src/app/_components/user-context.tsx
Normal file
46
src/app/_components/user-context.tsx
Normal 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 }
|
33
src/app/_data/entities/Ticket.ts
Normal file
33
src/app/_data/entities/Ticket.ts
Normal 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
|
||||||
|
|
||||||
|
}
|
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
|
|
45
src/app/_data/source/app-data-source.ts
Normal file
45
src/app/_data/source/app-data-source.ts
Normal 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;
|
25
src/app/_themes/KccbTheme.ts
Normal file
25
src/app/_themes/KccbTheme.ts
Normal 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'
|
||||||
|
});
|
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;
|
||||||
|
}
|
27
src/app/api/addColumn/route.ts
Normal file
27
src/app/api/addColumn/route.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
42
src/app/api/auth/login/account_no/route.ts
Normal file
42
src/app/api/auth/login/account_no/route.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
21
src/app/api/auth/logout/route.ts
Normal file
21
src/app/api/auth/logout/route.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
18
src/app/api/auth/session/options.ts
Normal file
18
src/app/api/auth/session/options.ts
Normal 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;
|
||||||
|
}
|
11
src/app/api/auth/session/payload.ts
Normal file
11
src/app/api/auth/session/payload.ts
Normal 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
2
src/app/api/auth/util.ts
Normal 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);
|
38
src/app/api/dev/populate/route.ts
Normal file
38
src/app/api/dev/populate/route.ts
Normal 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 });
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
36
src/app/api/mobile_no/by/[account_no]/route.ts
Normal file
36
src/app/api/mobile_no/by/[account_no]/route.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
9
src/app/api/otp/generate_otp.ts
Normal file
9
src/app/api/otp/generate_otp.ts
Normal 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
88
src/app/api/otp/route.ts
Normal 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
156
src/app/api/ticket/route.ts
Normal 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
39
src/app/api/user/route.ts
Normal 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
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);
|
||||||
|
}
|
||||||
|
|
160
src/app/home/layout.tsx
Normal file
160
src/app/home/layout.tsx
Normal 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>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
|
15
src/app/home/page.tsx
Normal file
15
src/app/home/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
265
src/app/home/user/raised-ticket/page.tsx
Normal file
265
src/app/home/user/raised-ticket/page.tsx
Normal 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;
|
178
src/app/home/user/view-ticket/page.tsx
Normal file
178
src/app/home/user/view-ticket/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
159
src/app/home/user/view-ticket/page.tsx.backupp
Normal file
159
src/app/home/user/view-ticket/page.tsx.backupp
Normal 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
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: "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
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
185
src/app/login/otp/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
95
src/app/login/page.tsx
Normal file
95
src/app/login/page.tsx
Normal 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
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='bottom-left' />
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<UserContextProvider>
|
||||||
|
{children}
|
||||||
|
</UserContextProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
)
|
||||||
|
}
|
16
test.js
Normal file
16
test.js
Normal 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
41
tsconfig.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
, "src/casbin/test.js", "src/app/home/admin/reset-user-password" ],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user