feat : connection made between login and home page

This commit is contained in:
2025-06-27 13:59:51 +05:30
parent 49dae3624f
commit 92531b02fd
7 changed files with 63 additions and 1048 deletions

View File

@@ -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);
}

View File

@@ -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>}
</>
)
}

View File

@@ -1,18 +1,38 @@
"use client"; "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() { export default function Home() {
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 ( return (
<> <>
<p>Welcome to IB Portal</p> <p>Welcome to IB portal</p>
<UserContextConsumer> <Button onClick={handleLogout}>Logout</Button>
{
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>
</> </>
); );
}
} }

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -7,12 +7,35 @@ import bgImage from '@/app/image/media.jpg';
import frontPage from '@/app/image/ib_front_page.jpg' import frontPage from '@/app/image/ib_front_page.jpg'
import NextImage from 'next/image'; import NextImage from 'next/image';
import { Providers } from "@/app/providers"; import { Providers } from "@/app/providers";
import { useRouter } from "next/navigation";
import { notifications } from "@mantine/notifications";
export default function Login() { export default function Login() {
const [CIF, SetCIF] = useState(''); const [CIF, SetCIF] = useState('');
const [psw, SetPsw] = 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 ( return (
@@ -83,7 +106,7 @@ export default function Login() {
<br></br> <br></br>
<Box style={{ width: "370px", padding: "20px", border: "1px solid #e0e0d1", marginLeft: 30 }}> <Box style={{ width: "370px", padding: "20px", border: "1px solid #e0e0d1", marginLeft: 30 }}>
<Flex justify="center" gap="md" > <Flex justify="center" gap="md" >
<form> <form onSubmit={handleLogin}>
<TextInput <TextInput
label="User ID" label="User ID"
placeholder="Enter your CIF no" placeholder="Enter your CIF no"
@@ -93,9 +116,8 @@ export default function Login() {
pattern="\d*" pattern="\d*"
value={CIF} value={CIF}
onInput={(e) => { onInput={(e) => {
const input =e.currentTarget.value.replace(/\D/g,''); const input = e.currentTarget.value.replace(/\D/g, '');
if(input.length<=11) if (input.length <= 11) {
{
SetCIF(input); SetCIF(input);
} }
}} }}
@@ -107,11 +129,13 @@ export default function Login() {
<PasswordInput <PasswordInput
label="Password" label="Password"
placeholder="Enter your password" placeholder="Enter your password"
value={psw}
onChange={(e) => SetPsw(e.currentTarget.value)}
style={{ flex: 1 }} style={{ flex: 1 }}
required required
/> />
{/* <br></br> */} {/* <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> </form>
</Flex> </Flex>
</Box> </Box>