feat : connection made between login and home page
This commit is contained in:
@@ -1,80 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
|
@@ -1,159 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React, { useContext, useState } from "react";
|
||||
import { UserContext } from "../_components/user-context";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { AppShell, Burger, Group, Image, Text, LoadingOverlay, Avatar, UnstyledButton, Title, Skeleton, Center, Switch, NavLink, ScrollArea, Code } from '@mantine/core';
|
||||
import { IconLogout, IconExclamationCircle, IconNotes, IconCategory, IconTicket, IconEye } from '@tabler/icons-react';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import classes from './layout.module.css';
|
||||
import { KccbTheme } from "../_themes/KccbTheme";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await axios.post('/api/auth/logout');
|
||||
} catch (error: AxiosError | any) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.code,
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default function HomeLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
const user = useContext(UserContext);
|
||||
if (user === null) redirect('/login');
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [burgerOpened, { toggle: toggleBurger }] = useDisclosure();
|
||||
const [activeNavItem, setActiveNavItem] = useState<string | null>(null);
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ['logout'],
|
||||
mutationFn: logout
|
||||
})
|
||||
|
||||
async function handleLogout(event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
await logoutMutation.mutateAsync();
|
||||
await queryClient.refetchQueries({ queryKey: ['user'] })
|
||||
}
|
||||
|
||||
let navItems: React.ReactNode[] = [];
|
||||
|
||||
navItems.push(<>
|
||||
<NavLink key={'raised-ticket'} label={
|
||||
<>
|
||||
<IconTicket size={20} style={{ marginRight: '5px', transform: 'translateY(5px)' }} />
|
||||
<span style={{ fontSize: '18px' }}>Create Ticket </span>
|
||||
</>
|
||||
}
|
||||
active={activeNavItem === 'raised-ticket' || undefined}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
setActiveNavItem('raised-ticket')
|
||||
router.push('/home/user/raised-ticket')
|
||||
}}
|
||||
/>
|
||||
<NavLink key={'view-ticket'} label={<><IconEye size={20} style={{ marginRight: '8px', transform: 'translateY(5px)' }} />
|
||||
<span style={{ fontSize: '18px' }}>View Ticket </span>
|
||||
</>
|
||||
}
|
||||
active={activeNavItem === 'view-ticket' || undefined}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
setActiveNavItem('view-ticket')
|
||||
router.push('/home/user/view-ticket');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay
|
||||
visible={!user || logoutMutation.isPending || logoutMutation.isSuccess}
|
||||
overlayProps={{ backgroundOpacity: 1 }} />
|
||||
|
||||
{user && <AppShell
|
||||
header={{ height: { base: 60, md: 70, lg: 80 } }}
|
||||
navbar={{
|
||||
width: { base: 200, md: 250, lg: 300 },
|
||||
breakpoint: 'sm',
|
||||
collapsed: { mobile: !burgerOpened },
|
||||
}}
|
||||
padding="lg"
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Group h="100%" px="md" justify="space-between">
|
||||
<Group>
|
||||
<Burger opened={burgerOpened} onClick={toggleBurger} hiddenFrom="sm" size="sm" />
|
||||
<Image
|
||||
radius='md'
|
||||
h={50}
|
||||
w='auto'
|
||||
fit='contain'
|
||||
src={'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-ZljkQXsXUP1-EwnIPZtVq_JWhwGUW7b0_eSMno-bag&s'}
|
||||
/>
|
||||
<Title order={2} className={classes.title}>Customer Request Management</Title>
|
||||
</Group>
|
||||
{/* <Group>
|
||||
|
||||
<UnstyledButton className={classes.user} onClick={() => {
|
||||
setActiveNavItem(null);
|
||||
router.push('/home/user');
|
||||
}}>
|
||||
</UnstyledButton>
|
||||
</Group> */}
|
||||
<Group>
|
||||
<Avatar radius="xl" src={null} alt={user.bank_account_no} color={KccbTheme.primaryColor}>
|
||||
</Avatar>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
Account No: [{user.bank_account_no}]
|
||||
</Text>
|
||||
{/* <Text c='dimmed' size="sm" component="span">User: </Text>
|
||||
<Code c='dimmed'>{user.id}</Code> */}
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
<AppShell.Navbar p="md">
|
||||
<Text>Menu</Text>
|
||||
<nav className={classes.navbar}>
|
||||
<div className={classes.navbarMain}>
|
||||
<ScrollArea>
|
||||
{navItems}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</nav>
|
||||
<div className={classes.footer}>
|
||||
<a className={classes.link} onClick={handleLogout}>
|
||||
<IconLogout className={classes.linkIcon} stroke={1.5} />
|
||||
<Text>Logout</Text>
|
||||
</a>
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main>
|
||||
{children}
|
||||
</AppShell.Main>
|
||||
</AppShell>}
|
||||
</>
|
||||
)
|
||||
}
|
@@ -1,18 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { UserContextConsumer } from "../_components/user-context";
|
||||
import { Button } from "@mantine/core";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
const[authorized,SetAuthorized] =useState<boolean|null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
async function handleLogout(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
localStorage.removeItem("access_token");
|
||||
router.push("/login")
|
||||
}
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else{
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if(authorized){
|
||||
return (
|
||||
<>
|
||||
<p>Welcome to IB Portal</p>
|
||||
<UserContextConsumer>
|
||||
{
|
||||
user => user && <p><b>Your Present Login Account No: {user.bank_account_no}</b></p>
|
||||
}
|
||||
</UserContextConsumer>
|
||||
<p><li>For raise a complain or assistance ,please click on <b>Create Ticket</b></li></p>
|
||||
<p><li>For track a ticket ,please click on <b>View Ticket</b></li></p>
|
||||
<p>Welcome to IB portal</p>
|
||||
<Button onClick={handleLogout}>Logout</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,263 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const ComplaintForm: React.FC = () => {
|
||||
const [category, setCategory] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [additionalFields, setAdditionalFields] = useState<string[]>([]);
|
||||
const [additionalFieldValues, setAdditionalFieldValues] = useState<Record<string, string>>({});
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedCategory = e.target.value;
|
||||
setCategory(selectedCategory);
|
||||
setDescription(""); // Reset description when category changes
|
||||
setAdditionalFields([]);
|
||||
setAdditionalFieldValues({});
|
||||
};
|
||||
|
||||
const handleDescriptionClick = () => {
|
||||
if (!category) {
|
||||
alert("Please select the Category of Complaint first.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!category) {
|
||||
setDescription(""); // Prevent changing description without category
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedDescription = e.target.value;
|
||||
setDescription(selectedDescription);
|
||||
|
||||
// Add additional fields for specific descriptions
|
||||
if (selectedDescription === "Transaction Issue") {
|
||||
setAdditionalFields([
|
||||
"Debit Card",
|
||||
"Transaction Amount",
|
||||
"Transaction Reference Number ",
|
||||
"Transaction Date",
|
||||
"ATM ID/Terminal ID",
|
||||
]);
|
||||
}
|
||||
else if(selectedDescription === "Fund Transfer failure"){
|
||||
setAdditionalFields([
|
||||
"Transaction Amount",
|
||||
"Transaction ID/Transaction Reference Number",
|
||||
"Transaction Date"
|
||||
]);
|
||||
}
|
||||
else {
|
||||
setAdditionalFields([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdditionalFieldChange = (field: string, value: string) => {
|
||||
if(field === "Transaction Amount" && isNaN(Number(value)))
|
||||
{
|
||||
alert ('Please enter a valid number');
|
||||
return;
|
||||
}
|
||||
if(field === "Debit Card" && isNaN(Number(value)))
|
||||
{
|
||||
alert ('Please Enter Valid Debit Card Number');
|
||||
return;
|
||||
}
|
||||
setAdditionalFieldValues((prevValues) => ({
|
||||
...prevValues,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
for (const field of additionalFields) {
|
||||
if (!additionalFieldValues[field]) {
|
||||
alert(`Please fill in the ${field}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!message.trim()) {
|
||||
alert("Message field is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const additionalInformation: string[] = [];
|
||||
if (additionalFieldValues['ATM ID/Terminal ID']) {
|
||||
additionalInformation.push(`ATM ID: ${additionalFieldValues['ATM ID/Terminal ID']}`);
|
||||
}
|
||||
if (additionalFieldValues['Debit Card']) {
|
||||
additionalInformation.push(`Debit Card: ${additionalFieldValues['Debit Card']}`);
|
||||
}
|
||||
if (additionalFieldValues['Transaction Amount']) {
|
||||
additionalInformation.push(`Transaction Amount: ${additionalFieldValues['Transaction Amount']}`);
|
||||
}
|
||||
if (additionalFieldValues['Transaction Reference Number ']) {
|
||||
additionalInformation.push(`Transaction Reference Number : ${additionalFieldValues['Transaction Reference Number ']}`);
|
||||
}
|
||||
if (additionalFieldValues['Transaction Date']) {
|
||||
additionalInformation.push(`Transaction Date: ${additionalFieldValues['Transaction Date']}`);
|
||||
}
|
||||
if(additionalFieldValues['Transaction ID/Transaction Reference Number']){
|
||||
additionalInformation.push(`Transaction ID/Transaction Reference Number: ${additionalFieldValues['Transaction ID/Transaction Reference Number']}`);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
summary: category,
|
||||
description: description,
|
||||
additional_information: additionalInformation.join(", "),
|
||||
steps_to_reproduce: message,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post("/api/ticket", requestBody);
|
||||
const data = await response.data;
|
||||
alert(data.message);
|
||||
window.location.reload();
|
||||
}
|
||||
catch (error: AxiosError | any) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px", maxWidth: "500px", textAlign: "left" }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<h2 style={{ marginBottom: "8px" }}> Create New Ticket </h2>
|
||||
|
||||
<label style={{ marginTop: "8px" }}>
|
||||
Category of Complaint <span style={{ color: "red" }}>*</span>
|
||||
<select
|
||||
value={category}
|
||||
onChange={handleCategoryChange}
|
||||
required
|
||||
style={{ display: "block", marginBottom: "10px", width: "100%" }}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Category
|
||||
</option>
|
||||
<option value="ATM Related">ATM Related</option>
|
||||
<option value="Internet Banking">Internet Banking</option>
|
||||
<option value="Mobile Banking">Mobile Banking</option>
|
||||
<option value="Others">Others</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Description <span style={{ color: "red" }}>*</span>
|
||||
<select
|
||||
value={description}
|
||||
onClick={handleDescriptionClick}
|
||||
onChange={handleDescriptionChange}
|
||||
required
|
||||
style={{ display: "block", marginBottom: "10px", width: "100%" }}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Description
|
||||
</option>
|
||||
{category === "ATM Related" && <option value="Transaction Issue">Transaction Issue</option>}
|
||||
{/* {category === "UPI" && <option value="UPI Issue">UPI Issue</option>} */}
|
||||
{category === "Internet Banking" && (
|
||||
<>
|
||||
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
|
||||
<option value="Fund Transfer failure">Fund Transfer Failure</option>
|
||||
<option value="Network Issue">Network Issue</option>
|
||||
</>
|
||||
)}
|
||||
{category === "Mobile Banking" && (
|
||||
<>
|
||||
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
|
||||
<option value="Fund Transfer failure">Fund Transfer Failure</option>
|
||||
<option value="Network Issue">Network Issue</option>
|
||||
</>
|
||||
)}
|
||||
{category === "Others" && (
|
||||
<>
|
||||
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
|
||||
<option value="Others">Others</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{additionalFields.map((field, index) => (
|
||||
<div key={index}>
|
||||
{field === "Transaction Date" ? (
|
||||
<label>
|
||||
{field}: <span style={{ color: "red" }}>*</span>
|
||||
<input
|
||||
type="date"
|
||||
value={additionalFieldValues[field] || ""}
|
||||
onChange={(e) => handleAdditionalFieldChange(field, e.target.value)}
|
||||
required
|
||||
style={{
|
||||
display: "block",
|
||||
marginBottom: "10px",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<label>
|
||||
{field}: <span style={{ color: "red" }}>*</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Enter ${field}`}
|
||||
value={additionalFieldValues[field] || ""}
|
||||
onChange={(e) => handleAdditionalFieldChange(field, e.target.value)}
|
||||
required
|
||||
style={{
|
||||
display: "block",
|
||||
marginBottom: "10px",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<label>
|
||||
Message (Max 500 characters) <span style={{ color: "red" }}>*</span>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
maxLength={500}
|
||||
rows={5}
|
||||
required
|
||||
style={{
|
||||
display: "block",
|
||||
marginBottom: "10px",
|
||||
width: "100%",
|
||||
resize: "none",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
backgroundColor: "#4CAF50",
|
||||
color: "white",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplaintForm;
|
@@ -1,332 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
Group,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconDisabled, IconEye, IconMessage, IconSearch, IconTrash ,IconArrowsUpDown} from "@tabler/icons-react";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
interface Message {
|
||||
note: string;
|
||||
time:string;
|
||||
}
|
||||
|
||||
interface Ticket {
|
||||
ticket_id: string;
|
||||
category_of_request: string;
|
||||
nature_of_request: string;
|
||||
created_date: string;
|
||||
status: string;
|
||||
message: Message[];
|
||||
}
|
||||
interface TicketDetails {
|
||||
ticket_id: string;
|
||||
category_of_request: string;
|
||||
nature_of_request: string;
|
||||
additional_info: string;
|
||||
message : string;
|
||||
created_date: string;
|
||||
|
||||
}
|
||||
export default function Page() {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
|
||||
const [activeMessages, setActiveMessages] = useState<Message[] | null>(null);
|
||||
const [ticketDetails, setTicketDetails] = useState<TicketDetails| null>(null);
|
||||
const [ticketStatus, setTicketStatus] = useState<string| null>(null);
|
||||
const [sortOrder,setSortOrder]= useState<"asc" |"desc">("desc");
|
||||
|
||||
const [detailsOpened, { open: openDetails, close: closeDetails }] = useDisclosure(false);
|
||||
|
||||
// For view ticket details
|
||||
const handleOpenDetails = async (ticketId: string) => {
|
||||
try {
|
||||
const response = await axios.get<TicketDetails>(`/api/ticket/${ticketId}`); // Assuming API provides details
|
||||
setTicketDetails(response.data);
|
||||
openDetails();
|
||||
}
|
||||
catch(error: AxiosError | any) {
|
||||
console.error("Failed to fetch ticket details:", error);
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.message
|
||||
})
|
||||
}
|
||||
};
|
||||
// For parsing additional_info
|
||||
const parseAdditionalInfo = (info: string) => {
|
||||
return info.split(", ").map((item) => {
|
||||
const [key, value] = item.split(": ");
|
||||
return { key: key.trim(), value: value?.trim() || "" };
|
||||
});
|
||||
};
|
||||
// For delete a ticket
|
||||
const handleDelete = async (ticketId: string) => {
|
||||
const isConfirm =window.confirm("Do you want to delete the ticket?");
|
||||
if(!isConfirm)
|
||||
return;
|
||||
try {
|
||||
const response = await axios.delete<TicketDetails>(`/api/ticket/${ticketId}`); // Assuming API provides details
|
||||
const data = await response.data;
|
||||
alert(data.message);
|
||||
}
|
||||
catch(error: AxiosError | any) {
|
||||
console.error("Failed to delete ticket:", error);
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.message
|
||||
})
|
||||
}
|
||||
finally{
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
// Sort and filter tickets based on the time
|
||||
useEffect(() => {
|
||||
const sortedTickets = [...tickets].sort(
|
||||
// (a, b) => new Date(b.created_date).getTime() - new Date(a.created_date).getTime()
|
||||
(a, b) => parseInt(b.ticket_id) - parseInt(a.ticket_id)
|
||||
);
|
||||
const results = sortedTickets.filter((ticket) =>
|
||||
ticket.category_of_request.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setFilteredTickets(results);
|
||||
}, [searchQuery, tickets]);
|
||||
|
||||
// Fetch tickets from API
|
||||
useEffect(() => {
|
||||
async function fetchTickets() {
|
||||
try {
|
||||
const response = await axios.get<Ticket[]>("/api/ticket");
|
||||
setTickets(response.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch tickets: ", error);
|
||||
}
|
||||
}
|
||||
fetchTickets();
|
||||
}, []);
|
||||
|
||||
// Sorting the ticket when any ticket resolved the go to the end of the list
|
||||
useEffect(() => {
|
||||
const sortedTickets = [...tickets].sort((a, b) => {
|
||||
if (a.status.toLowerCase() === "resolved" && b.status.toLowerCase() !== "resolved") return 1;
|
||||
if (b.status.toLowerCase() === "resolved" && a.status.toLowerCase() !== "resolved") return -1;
|
||||
return parseInt(b.ticket_id) - parseInt(a.ticket_id); // Sort by ticket ID otherwise
|
||||
});
|
||||
const results = sortedTickets.filter(ticket =>
|
||||
ticket.category_of_request.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setFilteredTickets(results);
|
||||
}, [searchQuery, tickets]);
|
||||
|
||||
// For sorting ticket history according to time
|
||||
const sortedMessages = activeMessages
|
||||
? [...activeMessages].sort((a, b) => {
|
||||
const timeA = new Date(a.time).getTime();
|
||||
const timeB = new Date(b.time).getTime();
|
||||
return sortOrder === "asc" ? timeA - timeB : timeB - timeA;
|
||||
})
|
||||
: [];
|
||||
|
||||
const handleOpenMessage = (messages: Message[],status:string) => {
|
||||
console.log(messages);
|
||||
setActiveMessages(messages);
|
||||
setTicketStatus(status);
|
||||
open();
|
||||
};
|
||||
|
||||
//Add conditional row styles
|
||||
const getRowStyle = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "resolved":
|
||||
// return { backgroundColor: "#d9d9d9", color: "#a6a6a6"}; // Grey for closed
|
||||
return {color: "#a6a6a6"}; // Grey for closed
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const rows = filteredTickets.map((ticket) => (
|
||||
<Table.Tr key={ticket.ticket_id} style={getRowStyle(ticket.status)}> {/* added for coloured grey */}
|
||||
{/* <Table.Tr key={ticket.ticket_id}> */}
|
||||
<Table.Td>{ticket.ticket_id}</Table.Td>
|
||||
<Table.Td>{ticket.category_of_request}</Table.Td>
|
||||
<Table.Td>{ticket.nature_of_request}</Table.Td>
|
||||
<Table.Td>{ticket.created_date}</Table.Td>
|
||||
<Table.Td>{ticket.status}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<ActionIcon style={getRowStyle(ticket.status)}
|
||||
variant="subtle" color="blue"
|
||||
onClick={() => handleOpenMessage(ticket.message,ticket.status)}
|
||||
>
|
||||
<Tooltip label="Ticket History">
|
||||
<IconMessage />
|
||||
</Tooltip>
|
||||
{/* design for badge */}
|
||||
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Group gap="md" wrap="nowrap" justify="center">
|
||||
<ActionIcon style={getRowStyle(ticket.status)}
|
||||
variant="subtle"
|
||||
onClick={() => handleOpenDetails(ticket.ticket_id)}>
|
||||
<Tooltip label="Details of the Ticket">
|
||||
<IconEye />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
|
||||
{/* Remove delete icon */}
|
||||
{/* <ActionIcon style={getRowStyle(ticket.status)}
|
||||
disabled={ticket.status.toLowerCase()==='resolved'}
|
||||
variant="subtle" color="red"
|
||||
onClick={() => handleDelete(ticket.ticket_id)}>
|
||||
<Tooltip label="Delete the Ticket">
|
||||
<IconTrash/>
|
||||
</Tooltip>
|
||||
</ActionIcon> */}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
));
|
||||
return (
|
||||
<Container fluid>
|
||||
<Title order={3}>View Ticket</Title>
|
||||
<Space h="1rem" />
|
||||
<Flex align="center" justify="space-between" w={720}>
|
||||
<Flex align="center">
|
||||
<Title order={5}>Tickets</Title>
|
||||
<Space w="1rem" />
|
||||
<TextInput
|
||||
placeholder="Search"
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.currentTarget.value)}
|
||||
radius="md"
|
||||
w={250}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Space h="1.5rem" />
|
||||
<Table
|
||||
w={720}
|
||||
stickyHeader
|
||||
stickyHeaderOffset={60}
|
||||
withTableBorder
|
||||
highlightOnHover
|
||||
horizontalSpacing="lg"
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Ticket ID</Table.Th>
|
||||
<Table.Th>Category</Table.Th>
|
||||
<Table.Th>Description</Table.Th>
|
||||
<Table.Th>Timestamp</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>History</Table.Th>
|
||||
<Table.Th>Action</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="">
|
||||
<Flex justify="space-between" align="center" >
|
||||
<Title order={4}>Ticket History</Title>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
|
||||
>
|
||||
<Tooltip label={`Sort by ${sortOrder === "asc" ? "Newest " : "Oldest"}`} position="right">
|
||||
<IconArrowsUpDown size={18} />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="sm" style={{ maxHeight: "400px", overflowY: "auto" }}>
|
||||
{sortedMessages?.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
padding: "10px",
|
||||
background: index % 2 === 0 ? "#f1f3f5" : "#dce6f2",
|
||||
borderRadius: "10px",
|
||||
maxWidth: "80%",
|
||||
alignSelf: "flex-start",
|
||||
boxShadow: "0 2px 5px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: 0, fontSize: "14px", color: "black",textAlign: "left" }}>{msg.note}</p>
|
||||
{/* //Fetch the note IST time */}
|
||||
<p style={{ margin: "6px 0 0", fontSize: "10px", color: "grey", textAlign: "right" }}>Internal team send by {new Date(Number(msg.time)*1000).toLocaleString('en-IN',{timeZone: 'Asia/Kolkata'})}</p>
|
||||
</div>
|
||||
))}
|
||||
{ticketStatus?.toLowerCase()=== "resolved" && (<div style={{ textAlign: "center", fontSize: "14px", color: "red", marginTop: "10px" }}>
|
||||
<p>----- Ended Chat ------</p>
|
||||
</div>)}
|
||||
{/* Fixed Close Button */}
|
||||
<div
|
||||
style={{
|
||||
position: "sticky",
|
||||
bottom: 0,
|
||||
background: "white",
|
||||
padding: "10px",
|
||||
textAlign: "center",
|
||||
boxShadow: "0px -5px 4px rgb(255, 255, 255)",
|
||||
}}
|
||||
>
|
||||
<Button onClick={close}>Close</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
</Modal>
|
||||
|
||||
<Modal opened={detailsOpened} onClose={closeDetails}>
|
||||
{ticketDetails ? (
|
||||
<Flex direction="column" gap="md">
|
||||
<div><strong>Ticket ID:</strong> {ticketDetails.ticket_id}</div>
|
||||
<div><strong>Category of Complaint:</strong> {ticketDetails.category_of_request}</div>
|
||||
<div><strong>Description:</strong> {ticketDetails.nature_of_request}</div>
|
||||
{/* <div><strong>Additional Information:</strong> {ticketDetails.additional_info}</div> */}
|
||||
{/* Additional Info Table */}
|
||||
<div><strong>Additional Info:</strong></div>
|
||||
<Table withTableBorder withColumnBorders highlightOnHover>
|
||||
<Table.Tbody>
|
||||
{parseAdditionalInfo(ticketDetails.additional_info).map((item, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{item.key}</Table.Td>
|
||||
<Table.Td>{item.value}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<div><strong>Message:</strong> {ticketDetails.message}</div>
|
||||
<div><strong>Created Date:</strong> {ticketDetails.created_date}</div>
|
||||
<Button onClick={closeDetails} mt="sm">Close</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<p>Loading details...</p>
|
||||
)}
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
}
|
@@ -1,195 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import classes from '../page.module.css';
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { TextInput, Button, Container, Title, Image, SimpleGrid, LoadingOverlay, Avatar, Paper, Text, Group, Center } from "@mantine/core";
|
||||
import image from '../helpdesk.png';
|
||||
import { useForm, SubmitHandler } from "react-hook-form";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
|
||||
type OtpInput = {
|
||||
OTP: number
|
||||
}
|
||||
|
||||
type Result = {
|
||||
ok?: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
async function handleResendOTP() {
|
||||
let otp;
|
||||
try {
|
||||
const response = await axios.get("/api/otp");
|
||||
otp = response.data;
|
||||
console.log('OTP resent successfully:', response.data);
|
||||
// Object.assign(response.data);
|
||||
// alert(response.data.message);
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
// title: error.code,
|
||||
message: response.data.message
|
||||
})
|
||||
} catch (error: AxiosError | any) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.code,
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
return otp;
|
||||
}
|
||||
|
||||
|
||||
async function handleValidateOTP(OtpInput: OtpInput) {
|
||||
let Result: Result = { ok: false }
|
||||
try {
|
||||
const response = await axios.post("/api/otp", OtpInput);
|
||||
{
|
||||
window.location.href = '/home'
|
||||
}
|
||||
Object.assign(Result, response.data);
|
||||
} catch (error: AxiosError | any) {
|
||||
// alert(error.response.data.error);
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.error,
|
||||
autoClose: 4000,
|
||||
})
|
||||
}
|
||||
// For countdown reset every time after successful verification
|
||||
sessionStorage.removeItem('countdown');
|
||||
sessionStorage.removeItem('timerStart');
|
||||
return Result;
|
||||
}
|
||||
|
||||
export default function CheckOTP() {
|
||||
const queryClient = useQueryClient();
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<OtpInput>();
|
||||
const [countdown, setCountdown] = useState<number>(90);
|
||||
const [timerActive, setTimerActive] = useState<boolean>(true);
|
||||
|
||||
const validateOTPMutation = useMutation<Result, AxiosError, OtpInput>({
|
||||
mutationKey: ['validateOtp'],
|
||||
mutationFn: handleValidateOTP,
|
||||
})
|
||||
const resendOTPMutation = useQuery({
|
||||
queryKey: ['resendOTP'],
|
||||
queryFn: handleResendOTP,
|
||||
enabled: false,
|
||||
})
|
||||
////
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedCountdown = sessionStorage.getItem('countdown');
|
||||
const savedStartTime = sessionStorage.getItem('timerStart');
|
||||
|
||||
if (savedCountdown && savedStartTime) {
|
||||
const elapsedTime = Math.floor((Date.now() - parseInt(savedStartTime, 10)) / 1000);
|
||||
const remainingTime = parseInt(savedCountdown, 10) - elapsedTime;
|
||||
|
||||
if (remainingTime > 0) {
|
||||
setCountdown(remainingTime);
|
||||
setTimerActive(true);
|
||||
}
|
||||
else {
|
||||
setCountdown(0);
|
||||
setTimerActive(false);
|
||||
sessionStorage.removeItem('countdown');
|
||||
sessionStorage.removeItem('timerStart');
|
||||
}
|
||||
} else {
|
||||
sessionStorage.setItem('countdown', '90');
|
||||
sessionStorage.setItem('timerStart', Date.now().toString());
|
||||
setCountdown(90);
|
||||
setTimerActive(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Effect to manage the countdown timer
|
||||
useEffect(() => {
|
||||
if (!timerActive) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval);
|
||||
setTimerActive(false);
|
||||
sessionStorage.removeItem('countdown');
|
||||
sessionStorage.removeItem('timerStart');
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval); // Cleanup interval on unmount
|
||||
}, [timerActive]);
|
||||
/////
|
||||
const onSubmit: SubmitHandler<OtpInput> = async (OtpInput) => {
|
||||
const Result = await validateOTPMutation.mutateAsync(OtpInput);
|
||||
if (Result.ok)
|
||||
await queryClient.refetchQueries({ queryKey: ['OTP'] });
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
resendOTPMutation.refetch();
|
||||
setCountdown(90);
|
||||
setTimerActive(true);
|
||||
sessionStorage.setItem('countdown', '90');
|
||||
sessionStorage.setItem('timerStart', Date.now().toString());
|
||||
}
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
<SimpleGrid spacing={{ base: 40, sm: 80 }} cols={{ base: 1, sm: 2 }}>
|
||||
<Image src={image.src} className={classes.mobileImage} />
|
||||
<div>
|
||||
<LoadingOverlay visible={validateOTPMutation.isPending || validateOTPMutation.data?.ok || resendOTPMutation.isLoading} />
|
||||
<Paper component='form' className={classes.form} radius={0} p={30} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Avatar
|
||||
src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-ZljkQXsXUP1-EwnIPZtVq_JWhwGUW7b0_eSMno-bag&s"
|
||||
alt="Jacob Warnhalter"
|
||||
radius="xxxl"
|
||||
/>
|
||||
<Title order={2} className={classes.title} ta="center" mt="md" mb={50}>
|
||||
Customer Request Management
|
||||
</Title>
|
||||
<TextInput label="Enter One-time password(OTP) *"
|
||||
placeholder="e.g. XXXXX"
|
||||
size="md"
|
||||
maxLength={6}
|
||||
{...register('OTP', { required: true })} />
|
||||
{errors.OTP && <Text c='red'>*Required</Text>}
|
||||
<Center mt="lg">
|
||||
<Text >{timerActive ? `Resend OTP in ${countdown}s` : 'Resend OTP Available'} </Text>
|
||||
</Center>
|
||||
<Group justify="center">
|
||||
<Button mt="xl" size="md" type='submit' >
|
||||
Validate OTP
|
||||
</Button>
|
||||
{validateOTPMutation.data?.message && <Text c='red' ta='center' pt={30}>{validateOTPMutation.data.message}</Text>}
|
||||
<Button mt="xl" size="md" type='submit' color='blue' onClick={handleResend} disabled={timerActive} >
|
||||
Resend OTP
|
||||
</Button>
|
||||
{/* {resendOTPMutation?.data?.message && <Text c='red' ta='center' pt={30}>{resendOTPMutation.data.message}</Text>} */}
|
||||
</Group>
|
||||
</Paper>
|
||||
</div>
|
||||
<Image src={image.src} className={classes.desktopImage} />
|
||||
</SimpleGrid>
|
||||
<footer justify-content='center' color='green'>Copyright © 2025 Tata Consultancy Services, All rights reserved.</footer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -7,12 +7,35 @@ import bgImage from '@/app/image/media.jpg';
|
||||
import frontPage from '@/app/image/ib_front_page.jpg'
|
||||
import NextImage from 'next/image';
|
||||
import { Providers } from "@/app/providers";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export default function Login() {
|
||||
const [CIF, SetCIF] = useState('');
|
||||
const [psw, SetPsw] = useState('');
|
||||
async function handleLogin() {
|
||||
const router = useRouter();
|
||||
|
||||
async function handleLogin(e:React.FormEvent) {
|
||||
e.preventDefault();
|
||||
// const login_password =localStorage.setItem("password",psw);
|
||||
if (CIF === "30022497139" && psw === "SecurePass123!")
|
||||
{
|
||||
const token ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30";
|
||||
localStorage.setItem("customerNumber",CIF);
|
||||
localStorage.setItem("password",psw);
|
||||
localStorage.setItem("access_token",token);
|
||||
router.push("/home");
|
||||
}
|
||||
|
||||
else {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: 'red',
|
||||
title: "Wrong User Id or Password",
|
||||
message: "Wrong User Id or Password",
|
||||
autoClose: 5000,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
return (
|
||||
@@ -83,7 +106,7 @@ export default function Login() {
|
||||
<br></br>
|
||||
<Box style={{ width: "370px", padding: "20px", border: "1px solid #e0e0d1", marginLeft: 30 }}>
|
||||
<Flex justify="center" gap="md" >
|
||||
<form>
|
||||
<form onSubmit={handleLogin}>
|
||||
<TextInput
|
||||
label="User ID"
|
||||
placeholder="Enter your CIF no"
|
||||
@@ -93,13 +116,12 @@ export default function Login() {
|
||||
pattern="\d*"
|
||||
value={CIF}
|
||||
onInput={(e) => {
|
||||
const input =e.currentTarget.value.replace(/\D/g,'');
|
||||
if(input.length<=11)
|
||||
{
|
||||
SetCIF(input);
|
||||
}
|
||||
const input = e.currentTarget.value.replace(/\D/g, '');
|
||||
if (input.length <= 11) {
|
||||
SetCIF(input);
|
||||
}
|
||||
}}
|
||||
|
||||
|
||||
style={{ flex: 1 }}
|
||||
required
|
||||
/>
|
||||
@@ -107,11 +129,13 @@ export default function Login() {
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
value={psw}
|
||||
onChange={(e) => SetPsw(e.currentTarget.value)}
|
||||
style={{ flex: 1 }}
|
||||
required
|
||||
/>
|
||||
{/* <br></br> */}
|
||||
<Button fullWidth variant="filled" mt="md" style={{ width: 100, align: "center", marginLeft: 40 }}>Login</Button>
|
||||
<Button type="submit" fullWidth variant="filled" mt="md" style={{ width: 100, align: "center", marginLeft: 40 }}>Login</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
Reference in New Issue
Block a user