feat: Create login page for admin.

wip: Admin user Configuration
This commit is contained in:
2025-08-08 14:57:33 +05:30
parent 3ee40cad55
commit b2e84608c3
13 changed files with 945 additions and 18 deletions

View File

@@ -0,0 +1,243 @@
import React, { useState } from 'react';
import { TextInput, Button, Title, Stack, Radio, Group, Text, Divider, LoadingOverlay, Box, Modal, Card, Checkbox } from '@mantine/core';
import { notifications } from '@mantine/notifications';
export default function UserConfiguration() {
const [CIF, setCIF] = useState('');
const [userDetails, setUserDetails] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [savingsAccount, setSavingsAccount] = useState<string | null>(null);
const [accountError, setAccountError] = useState<string>('');
const [detailsExpanded, setDetailsExpanded] = useState(true);
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [confirmedPreview, setConfirmedPreview] = useState(false);
const [ibEnabled, setIbEnabled] = useState(false);
const [ibAccess,setIbAccess] =useState<"read"|"transaction"|"">("");
const [mbEnabled, setMbEnabled] = useState(false);
const [mbAccess,setMbAccess] =useState<"read"|"transaction"|"">("");
const isValidated = !!userDetails;
const canSubmit = isValidated && !!savingsAccount && confirmedPreview;
const handleValidate = async () => {
if (!CIF) {
notifications.show({
color: 'red',
title: 'CIF Missing',
message: 'Please enter a CIF number to proceed.'
});
return;
}
setLoading(true);
setAccountError('');
setSavingsAccount(null);
setUserDetails(null);
try {
const token = localStorage.getItem("admin_access_token");
const response = await fetch(`/api/auth/admin/fetch/customer_details?CIF=${CIF}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
});
const data = await response.json();
if (response.ok && Array.isArray(data) && data.length > 0) {
const saAccount = data.find(acc => acc.stAccountType === 'SA');
console.log(saAccount);
if (saAccount) {
setSavingsAccount(saAccount.stAccountNo);
} else {
setAccountError('At least one savings account is required for requesting.');
}
const user = data[0];
setUserDetails({
name: user.custname,
userId: user.id,
mobile: user.mobileno,
address: user.custaddress,
activeAccount: user.activeAccounts,
});
} else {
throw new Error('User not found or data format incorrect');
}
} catch (err: any) {
notifications.show({
color: 'red',
title: 'Validation Failed',
message: 'User not found',
});
} finally {
setLoading(false);
}
};
const handleSubmit = () => {
if (!canSubmit) {
notifications.show({
color: 'red',
title: 'Required',
message: 'One savings account is required for enable rights',
});
return;
}
else {
const rightsData = {
CIF,
// internetBanking,
// mobileBanking,
};
console.log('Submitting rights:', rightsData);
notifications.show({
color: 'blue',
title: 'Submitted',
message: `User -${CIF} rights submitted successfully.`,
});
}
setSavingsAccount(null);
setUserDetails(null);
setCIF('');
};
const handleMainAction = () => {
if (!isValidated) {
handleValidate();
} else if (!confirmedPreview) {
setShowPreviewModal(true);
} else if (isValidated && confirmedPreview) {
handleSubmit();
}
};
return (
<Box>
<LoadingOverlay visible={loading} zIndex={1000} />
<Title order={4}>User Configuration</Title>
<TextInput
label="Enter CIF No"
placeholder="Enter your CIF No"
value={CIF}
onChange={(e) => {
const input = e.currentTarget.value.replace(/\D/g, '');
if (input.length <= 11) setCIF(input);
}}
disabled={isValidated}
withAsterisk />
{isValidated && (
<>
<Divider size="xs" label="User Info" />
<Box style={{ overflowX: 'auto', border: '1px solid #eee', borderRadius: 8, padding: 12 }}>
<Group p="apart">
<Text fw={500}>Preview User Details</Text>
<Button variant="subtle" size="xs" onClick={() => setDetailsExpanded((v) => !v)}>
{detailsExpanded ? '▲' : '▼'}
</Button>
</Group>
{detailsExpanded && (
<>
<Group grow>
<TextInput label="User ID" value={userDetails.userId} readOnly disabled />
<TextInput label="Name" value={userDetails.name} readOnly disabled />
<TextInput label="Mobile" value={userDetails.mobile} readOnly disabled />
</Group>
<Group grow>
<TextInput label="Address" value={userDetails.address} readOnly disabled />
<TextInput label="Savings Account" value={savingsAccount || ''} readOnly disabled />
<TextInput label="Active Accounts" value={userDetails.activeAccount} readOnly disabled />
</Group>
</>
)}
</Box>
<Divider label='Rights' size="xs" />
<Stack>
<Group grow>
<Box>
<Text fw={500}>Internet Banking</Text>
<Group mt="xs">
<Checkbox
label="Enabled"
checked={ibEnabled}
onChange={(e) => {
setIbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setIbAccess(""); // reset if disabled
}}
/>
<Checkbox
label="Read"
disabled={!ibEnabled}
checked={ibAccess === "read"}
onChange={() => setIbAccess("read")}
/>
<Checkbox
label="Transaction"
disabled={!ibEnabled}
checked={ibAccess === "transaction"}
onChange={() => setIbAccess("transaction")}
/>
</Group>
</Box>
{/* Mobile Banking */}
<Box>
<Text fw={500}>Mobile Banking</Text>
<Group mt="xs">
<Checkbox
label="Enabled"
checked={mbEnabled}
onChange={(e) => {
setMbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setMbAccess(""); // reset if disabled
}}
/>
<Checkbox
label="Read"
disabled={!mbEnabled}
checked={mbAccess === "read"}
onChange={() => setMbAccess("read")}
/>
<Checkbox
label="Transaction"
disabled={!mbEnabled}
checked={mbAccess === "transaction"}
onChange={() => setMbAccess("transaction")}
/>
</Group>
</Box>
</Group>
</Stack>
{accountError && (
<Text color="red" size="sm" mt="xs">{accountError}</Text>
)}
</>
)}
<Button mt="lg" onClick={handleMainAction} disabled={loading}>
{!isValidated ? 'Validate' : !confirmedPreview ? 'Preview' : 'Submit'}
</Button>
<Modal
opened={showPreviewModal}
onClose={() => setShowPreviewModal(false)}
title="Confirm User Rights"
centered
>
<Stack>
<TextInput label="CIF" value={CIF} readOnly disabled />
{/* <TextInput label="Internet Banking" value={internetBanking} readOnly disabled />
<TextInput label="Mobile Banking" value={mobileBanking} readOnly disabled /> */}
</Stack>
<Group p="right" mt="md">
<Button variant="default" onClick={() => setShowPreviewModal(false)}>Cancel</Button>
<Button onClick={() => { setConfirmedPreview(true); setShowPreviewModal(false); }}>OK</Button>
</Group>
</Modal>
</Box>
);
}

View File

@@ -0,0 +1,162 @@
"use client";
import React, { useState, useEffect } from "react";
import { Text, Box, Image, Button, Stack, Divider, Title } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { Providers } from "@/app/providers";
import { useRouter } from "next/navigation";
import NextImage from "next/image";
import logo from '@/app/image/logo.jpg';
import { IconLogout, IconUsers, IconUserScreen } from "@tabler/icons-react";
import UserConfiguration from "./UserConfiguration";
export default function Login() {
const router = useRouter();
const [authorized, SetAuthorized] = useState<boolean | null>(null);
const [view, setView] = useState<'userConf' | 'view' | null>(null);
const [lastLoginDetails, setLastLoginDetails] = useState(null);
const [name, setName] = useState(null);
async function handleLogout(e: React.FormEvent) {
e.preventDefault();
localStorage.removeItem("admin_access_token");
router.push("/administrator/login");
}
async function handleFetchUserDetails(e: React.FormEvent) {
e.preventDefault();
const token = localStorage.getItem("admin_access_token");
const response = await fetch('/api/auth/admin/admin_details', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
});
const data = await response.json();
// console.log(data)
if (response.ok) {
return data;
}
else if (response.status === 401 || data.message === 'invalid or expired token') {
localStorage.removeItem("admin_access_token");
router.push('/administrator/login');
}
else {
notifications.show({
withBorder: true,
color: "red",
title: "Please try again later",
message: "Unable to fetch timestamp, please try again later",
autoClose: 5000,
});
}
}
useEffect(() => {
const token = localStorage.getItem("admin_access_token");
if (!token) {
SetAuthorized(false);
router.push("/administrator/login/");
} else {
SetAuthorized(true);
const fetchLoginTime = async () => {
const result = await handleFetchUserDetails({ preventDefault: () => { } } as React.FormEvent);
if (result) {
setLastLoginDetails(result.last_login);
setName(result.name);
}
};
fetchLoginTime();
}
}, []);
const handleClick = (type: 'UserConf' | 'ViewUser' | 'logout') => {
if (type === 'UserConf') {
setView('userConf');
} else if (type === 'ViewUser') {
setView('view');
}
};
if (!authorized) return null;
if (authorized) {
return (
<Providers>
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "100%", paddingTop: "5%" }}>
{/* Header */}
<Box style={{
position: 'fixed', width: '100%', height: '12%', top: 0, left: 0, zIndex: 100,
display: "flex",
justifyContent: "flex-start",
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)",
// border: "1px solid black"
}}>
<Image
fit="cover"
src={logo}
component={NextImage}
alt="ebanking"
style={{ width: "40%", height: "100%", objectFit: "contain", marginLeft: 0 }}
/>
<Text
style={{
position: 'absolute',
top: '50%',
left: '64%',
color: 'white',
textShadow: '1px 1px 2px blue',
}}
>
{/* <IconBuildingBank/> */}
Head Office : Dharmshala, District: Kangra(H.P), Pincode: 176215
</Text>
</Box>
{/* layout and */}
<Box style={{ display: 'flex', height: '88vh' }}>
{/* Sidebar manually placed under header */}
<Box
style={{
width: 220,
background:"#ebf5f5ff",
// background: "linear-gradient(90deg,rgba(42, 123, 155, 1) 0%, rgba(87, 199, 133, 1) 30%)",
padding: '1rem',
borderRight: '1px solid #ccc',
}}
>
<Title order={3} style={{textAlign:'center'}}>Admin Portal</Title>
<Divider my="xs" label="Menu" labelPosition="center" />
<Stack>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('UserConf')}>
<IconUsers /> User Configuration
</Text>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('ViewUser')}>
<IconUserScreen /> View Users
</Text>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={handleLogout}>
<IconLogout /> Logout
</Text>
</Stack>
</Box>
{/* Main Content Area */}
<Stack style={{ flex: 1, padding: '1rem' }}>
<Box>
<Text c="Blue" size="lg" fs='italic'>Welcome ,{name} </Text>
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: '13px' }}>
Last logged in at {lastLoginDetails ? new Date(lastLoginDetails).toLocaleString() : "N/A"}
</Text>
</Box>
<Box>
{view === 'userConf' && <UserConfiguration />}
{view === 'view' && <Text size="xl">view</Text>}
{!view &&
<Text size="xl">Welcome To The Admin Portal</Text>
}
</Box>
</Stack>
</Box>
</div>
</Providers>
);
}
}

View File

@@ -0,0 +1,207 @@
"use client";
import React, { useState, useEffect } from "react";
import { Text, Button, TextInput, PasswordInput, Title, Card, Group, Box, Image } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { Providers } from "@/app/providers";
import { useRouter } from "next/navigation";
import NextImage from "next/image";
import logo from '@/app/image/logo.jpg';
import frontPage from '@/app/image/admin_login.jpg';
import { generateCaptcha } from '@/app/captcha';
export default function Login() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [userName, setUserName] = useState("");
const [psw, SetPsw] = useState("");
const [captcha, setCaptcha] = useState("");
const [inputCaptcha, setInputCaptcha] = useState("");
const [isLogging, setIsLogging] = useState(false);
useEffect(() => {
const loadCaptcha = async () => {
const newCaptcha = await generateCaptcha();
setCaptcha(newCaptcha);
};
loadCaptcha();
}, []);
const regenerateCaptcha = () => {
// setCaptcha(generateCaptcha());
const loadCaptcha = async () => {
const newCaptcha = await generateCaptcha();
setCaptcha(newCaptcha);
};
loadCaptcha();
setInputCaptcha("");
};
async function handleLogin(e: React.FormEvent) {
e.preventDefault();
if (!userName || !psw || !captcha) {
notifications.show({
withBorder: true,
color: "red",
title: "Field Blank",
message: "Please enter all mandatory details",
autoClose: 5000,
});
return;
}
if (inputCaptcha !== captcha) {
notifications.show({
withBorder: true,
color: "red",
title: "Captcha Error",
message: "Please enter the correct captcha",
autoClose: 5000,
});
regenerateCaptcha();
return;
}
const response = await fetch('/api/auth/admin/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userName: userName,
password: psw,
}),
});
const data = await response.json();
setIsLogging(true);
if (response.ok) {
console.log(data);
const token = data.token;
localStorage.setItem("admin_access_token", token);
router.push("/administrator/home");
}
else {
setIsLogging(false);
notifications.show({
withBorder: true,
color: "red",
title: "Wrong User Id or Password",
message: "Wrong User Id or Password",
autoClose: 5000,
});
}
}
return (
<Providers>
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "100%", paddingTop: "5%" }}>
{/* Header */}
<Box style={{
position: 'fixed', width: '100%', height: '12%', top: 0, left: 0, zIndex: 100,
display: "flex",
justifyContent: "flex-start",
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)",
// border: "1px solid black"
}}>
<Image
fit="cover"
src={logo}
component={NextImage}
alt="ebanking"
style={{ width: "40%", height: "100%", objectFit: "contain", marginLeft: 0 }}
/>
<Text
style={{
position: 'absolute',
top: '50%',
left: '64%',
color: 'white',
textShadow: '1px 1px 2px blue',
}}
>
{/* <IconBuildingBank/> */}
Head Office : Dharmshala, District: Kangra(H.P), Pincode: 176215
</Text>
</Box>
<div>
{/* Main */}
<div style={{
display: "flex", height: "84vh", overflow: "hidden", position: "relative",
background: 'linear-gradient(to right, #48ac64ff, #199444ff)'
}}>
<div style={{ flex: 1, backgroundColor: "#c1e0f0", position: "relative" }}>
<Image
fit="cover"
src={frontPage}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "100%" }}
/>
</div>
<Box w={{ base: "100%", md: "50%" }} p="lg">
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 500, margin: "0 auto", height: '70vh', backdropFilter: 'blur(8px)' }}>
{/* @ts-ignore */}
<Title order={3} align='center'>Admin Portal</Title>
<form onSubmit={handleLogin}>
<TextInput
label="User Name"
placeholder="Enter your user name"
value={userName}
onChange={(e) => setUserName(e.currentTarget.value)}
error={error}
withAsterisk
/>
<PasswordInput
label="Password"
placeholder="Enter your password"
value={psw}
onChange={(e) => SetPsw(e.currentTarget.value)}
withAsterisk
mt="sm"
/>
{/* <Box style={{ textAlign: "right"}}>
<Anchor
style={{ fontSize: "14px", color: "#1c7ed6", cursor: "pointer" }}
// onClick={() => router.push("/ValidateUser")}
>
Forgot Password?
</Anchor>
</Box> */}
<Group mt="sm" align="center">
<Box style={{ backgroundColor: "#fff", fontSize: "18px", textDecoration: "line-through", padding: "4px 8px", fontFamily: "cursive" }}>{captcha}</Box>
<Button size="xs" variant="light" onClick={regenerateCaptcha}>Refresh</Button>
</Group>
<TextInput
label="Enter CAPTCHA"
placeholder="Enter above text"
value={inputCaptcha}
onChange={(e) => setInputCaptcha(e.currentTarget.value)}
withAsterisk
mt="sm"
/>
<Button type="submit" fullWidth mt="md" disabled={isLogging}>
{isLogging ? "Logging..." : "Login"}
</Button>
</form>
</Card>
</Box>
</div>
{/* Footer */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
fontSize: "12px",
}}
>
<Text c='dimmed'>
© 2025 KCC Bank. All rights reserved.
</Text>
</Box>
</div>
</div>
</Providers>
);
}