feat : screen for user create and update

feat : unlocked users.
This commit is contained in:
2025-11-04 13:05:00 +05:30
parent 96b505a38c
commit 26b9eff409
6 changed files with 982 additions and 404 deletions

View File

@@ -14,8 +14,8 @@ interface SendOtpPayload {
} }
function getStoredMobileNumber(): string { function getStoredMobileNumber(): string {
const mobileNumber = localStorage.getItem('remitter_mobile_no'); // const mobileNumber = localStorage.getItem('remitter_mobile_no');
// const mobileNumber = "7890544527"; const mobileNumber = "7890544527";
if (!mobileNumber) throw new Error('Mobile number not found.'); if (!mobileNumber) throw new Error('Mobile number not found.');
return mobileNumber; return mobileNumber;
} }

View File

@@ -0,0 +1,265 @@
"use client";
import React, { useState, useEffect } from "react";
import {
TextInput,
Button,
Title,
Stack,
Group,
Text,
Paper,
LoadingOverlay,
Divider,
Select,
Center,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation";
export default function UserUnlockPage() {
const router = useRouter();
const [CIF, setCIF] = useState("");
const [username, setUsername] = useState("");
const [userDetails, setUserDetails] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Redirect if not logged in
useEffect(() => {
const token = localStorage.getItem("admin_access_token");
if (!token) {
localStorage.clear();
sessionStorage.clear();
router.push("/administrator/login");
}
}, [router]);
const checkAuth = () => {
const token = localStorage.getItem("admin_access_token");
if (!token) {
localStorage.clear();
sessionStorage.clear();
router.push("/administrator/login");
return null;
}
return token;
};
// Fetch user info
const handleSearch = async () => {
setError(null);
setUserDetails(null);
if (!CIF && !username) {
notifications.show({
color: "red",
title: "Missing Input",
message: "Please enter either CIF number or username.",
});
return;
}
setLoading(true);
const token = checkAuth();
if (!token) return;
try {
const query = CIF ? CIF : username;
const response = await fetch(`/api/auth/admin/user/rights?CIF=${query}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "Admin",
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
if (!response.ok || !data?.customer_no) {
setError("User not found or invalid CIF/username.");
notifications.show({
color: "red",
title: "User Not Found",
message: "Invalid CIF or username.",
});
return;
}
setUserDetails(data);
notifications.show({
color: data.locked ? "red" : "green",
title: "User Found",
message: `User ${data.customer_no} is currently ${data.locked ? "Locked 🔒" : "Unlocked 🔓"
}`,
});
} catch (err) {
console.error(err);
notifications.show({
color: "red",
title: "Error",
message: "Failed to fetch user details.",
});
} finally {
setLoading(false);
}
};
// 🔄 Unlock or lock user
const handleStatusChange = async (value: string) => {
if (!userDetails?.customer_no) return;
const isUnlock = value === "unlocked";
const token = checkAuth();
if (!token) return;
setLoading(true);
try {
const response = await fetch("/api/auth/admin/user/unlock", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "Admin",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
user: userDetails.customer_no, // can also be username if API supports
action: !isUnlock, // false => unlock, true => lock
}),
});
const data = await response.json();
if (response.ok) {
setUserDetails({ ...userDetails, locked: !isUnlock });
notifications.show({
color: "green",
title: "Success",
message: `User ${userDetails.customer_no} ${isUnlock ? "unlocked" : "locked"
} successfully.`,
});
} else {
notifications.show({
color: "red",
title: "Failed",
message: data.message || "Unable to update lock status.",
});
}
} catch (err) {
console.error(err);
notifications.show({
color: "red",
title: "Error",
message: "Something went wrong while updating user status.",
});
} finally {
setLoading(false);
}
};
const handleReset = () => {
setCIF("");
setUsername("");
setUserDetails(null);
setError(null);
};
return (
<>
<Title order={4}>User Unlock Management</Title>
<Paper p="md" mt="md" radius="md" withBorder>
<LoadingOverlay visible={loading} zIndex={1000} />
<Group grow>
<TextInput
label="CIF Number"
placeholder="Enter CIF Number"
value={CIF}
onChange={(e) => {
const input = e.currentTarget.value.replace(/\D/g, "");
if (input.length <= 11) setCIF(input);
setError(null);
}}
disabled={!!username}
/>
<Text c="dimmed" ta="center">
OR
</Text>
<TextInput
label="Username"
placeholder="Enter Username"
value={username}
onChange={(e) => {
const sanitized = e.currentTarget.value.replace(/[^a-zA-Z0-9@_]/g, "");
if (sanitized.length <= 15) setUsername(sanitized);
setError(null);
}}
disabled={!!CIF}
/>
</Group>
{error && (
<Center mt="xs">
<Text c="red" size="sm" fw={500}>
{error}
</Text>
</Center>
)}
<Group mt="md">
<Button onClick={handleSearch} disabled={loading}>
Search
</Button>
<Button variant="light" color="red" onClick={handleReset}>
Reset
</Button>
</Group>
</Paper>
{userDetails && (
<Paper withBorder p="md" mt="xl" radius="md">
<Stack gap="xs">
<Divider label="User Details" labelPosition="center" />
<Text>
<strong>Customer No:</strong> {userDetails.customer_no}
</Text>
<Text>
<strong>Registration Status:</strong>{" "}
{userDetails.last_login === null ? (
<Text span c="red" fw={600}>
Not Registered
</Text>
) : (
<Text span c="green" fw={600}>
Registered
</Text>
)}
</Text>
<Select
label="Locked Status"
value={userDetails.locked ? "locked" : "unlocked"}
data={[
{ value: "locked", label: "Locked 🔒" },
{ value: "unlocked", label: "Unlocked 🔓" },
]}
onChange={(value) => {
if (!value) return;
handleStatusChange(value);
}}
mt="md"
disabled={!userDetails.locked} // only allow change if locked
/>
</Stack>
</Paper>
)}
</>
);
}

View File

@@ -15,10 +15,11 @@ import {
Paper, Paper,
Tabs, Tabs,
ScrollArea, ScrollArea,
Center,
} from "@mantine/core"; } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { IconGavel, IconUser } from "@tabler/icons-react"; import { IconGavel, IconHome, IconUser } from "@tabler/icons-react";
export default function UserConfiguration() { export default function UserConfiguration() {
const router = useRouter(); const router = useRouter();
@@ -32,11 +33,15 @@ export default function UserConfiguration() {
const [ibAccess, setIbAccess] = useState<"2" | "1" | null>(null); const [ibAccess, setIbAccess] = useState<"2" | "1" | null>(null);
const [mbEnabled, setMbEnabled] = useState(false); const [mbEnabled, setMbEnabled] = useState(false);
const [mbAccess, setMbAccess] = useState<"2" | "1" | null>(null); const [mbAccess, setMbAccess] = useState<"2" | "1" | null>(null);
const [ibLimit, setIbLimit] = useState("");
const [mbLimit, setMbLimit] = useState("");
const [error, setError] = useState<string | null>(null);
const isValidated = !!userDetails; const isValidated = !!userDetails;
const canSubmit = const canSubmit =
isValidated && savingsAccount && userDetails?.mobile && confirmedPreview; isValidated && savingsAccount && userDetails?.mobile && confirmedPreview;
// 🔹 Fetch both user details and rights
const handleValidate = async () => { const handleValidate = async () => {
if (!CIF) { if (!CIF) {
notifications.show({ notifications.show({
@@ -55,48 +60,74 @@ export default function UserConfiguration() {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Login-Type": "Admin", "X-Login-Type": "Admin",
Authorization: `Bearer ${token}`,
},
}
);
const data = await response.json();
if (!response.ok || !Array.isArray(data) || data.length === 0) {
setError("User not present or invalid CIF number.");
}
const saAccount = data.find(
(acc: { stAccountType: string; }) => acc.stAccountType === "SA" || acc.stAccountType === "SB"
);
setSavingsAccount(saAccount?.stAccountNo ?? null);
const user = data[0];
setUserDetails({
name: user.custname,
userId: user.id,
mobile: user.mobileno ?? null,
address: user.custaddress,
activeAccount: user.activeAccounts,
branchNo: user.stBranchNo,
dateOfBirth: user.custdob,
pinCode: user.pincode,
});
// Fetch existing rights if any
const rightsRes = await fetch(
`/api/auth/admin/user/rights?CIF=${CIF}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "Admin",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
} }
); );
const data = await response.json(); if (rightsRes.ok) {
if (response.ok && Array.isArray(data) && data.length > 0) { const rights = await rightsRes.json();
const saAccount = data.find((acc) => acc.stAccountType === "SA"); if (rights?.ib_access_level || rights?.mb_access_level) {
setSavingsAccount(saAccount?.stAccountNo ?? null); // populate existing rights
if (rights.ib_access_level) {
const user = data[0]; setIbEnabled(true);
setUserDetails({ setIbAccess(rights.ib_access_level as "1" | "2");
name: user.custname, setIbLimit(rights.inb_limit_amount?.toString() || "");
userId: user.id, }
mobile: user.mobileno ?? null, if (rights.mb_access_level) {
address: user.custaddress, setMbEnabled(true);
activeAccount: user.activeAccounts, setMbAccess(rights.mb_access_level as "1" | "2");
}); setMbLimit(rights.mobile_limit_amount?.toString() || "");
} else { }
notifications.show({ }
color: "red",
title: "Validation Failed",
message: "User not found",
});
} }
} catch (err) {
}
catch (err) {
console.error(err); console.error(err);
notifications.show({ setError("User not present or invalid CIF number.");
color: "red",
title: "Validation Failed",
message: "User not found",
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// Preview before submit
const handlePreview = () => { const handlePreview = () => {
if (!savingsAccount || !userDetails?.mobile) { if (!savingsAccount || !userDetails?.mobile) return;
return;
}
if (!ibAccess && !mbAccess) { if (!ibAccess && !mbAccess) {
notifications.show({ notifications.show({
title: "Rights Required", title: "Rights Required",
@@ -106,12 +137,20 @@ export default function UserConfiguration() {
}); });
return; return;
} }
if ((ibEnabled && !ibLimit) || (mbEnabled && !mbLimit)) {
notifications.show({
title: "Limit Required",
message: "Please enter fund transfer limits for enabled services.",
color: "red",
});
return;
}
setShowPreviewModal(true); setShowPreviewModal(true);
}; };
// Submit or update rights
const handleSubmit = async () => { const handleSubmit = async () => {
if (!canSubmit) return; if (!canSubmit) return;
try { try {
const token = localStorage.getItem("admin_access_token"); const token = localStorage.getItem("admin_access_token");
const response = await fetch("/api/auth/admin/user/rights", { const response = await fetch("/api/auth/admin/user/rights", {
@@ -125,52 +164,58 @@ export default function UserConfiguration() {
CIF, CIF,
ib_access_level: ibAccess, ib_access_level: ibAccess,
mb_access_level: mbAccess, mb_access_level: mbAccess,
ib_limit: ibLimit ? parseInt(ibLimit) : 0,
mb_limit: mbLimit ? parseInt(mbLimit) : 0,
}), }),
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
if (data?.otp) { if (data?.otp) {
// Registration case await fetch("/api/otp/send", {
await fetch('/api/otp/send', { method: "POST",
method: 'POST', headers: {
headers: { 'Content-Type': 'application/json',"X-Login-Type": "Admin", }, "Content-Type": "application/json",
"X-Login-Type": "Admin",
},
body: JSON.stringify({ body: JSON.stringify({
mobileNumber: userDetails.mobile, mobileNumber: userDetails.mobile,
type: 'REGISTRATION', type: "REGISTRATION",
userOtp: data.otp, userOtp: data.otp,
}), }),
}); });
notifications.show({ notifications.show({
color: 'blue', color: "blue",
title: 'Submitted', title: "Submitted",
message: `User ${CIF} rights submitted successfully. OTP sent to the user.`, message: `User ${CIF} rights submitted successfully. OTP sent to the user.`,
}); });
} } else if (data?.message) {
else if (data?.message) { await fetch("/api/otp/send", {
// Rights update case method: "POST",
await fetch('/api/otp/send', { headers: {
method: 'POST', "Content-Type": "application/json",
headers: { 'Content-Type': 'application/json',"X-Login-Type": "Admin", }, "X-Login-Type": "Admin",
},
body: JSON.stringify({ body: JSON.stringify({
mobileNumber: userDetails.mobile, mobileNumber: userDetails.mobile,
type: 'RIGHT_UPDATE', type: "RIGHT_UPDATE",
}), }),
}); });
notifications.show({ notifications.show({
color: 'blue', color: "blue",
title: 'Submitted', title: "Updated",
message: `User ${CIF} rights updated successfully. Confirmation sent to the user.`, message: `User ${CIF} rights updated successfully. Confirmation sent to the user.`,
}); });
} }
} } else if (
else if (response.status === 401 || data.message === "invalid or expired token") { response.status === 401 ||
data.message === "invalid or expired token"
) {
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
router.push("/administrator/login"); router.push("/administrator/login");
} }
} } catch (err) {
catch (err) {
console.error(err); console.error(err);
notifications.show({ notifications.show({
color: "red", color: "red",
@@ -190,20 +235,19 @@ export default function UserConfiguration() {
setConfirmedPreview(false); setConfirmedPreview(false);
setIbEnabled(false); setIbEnabled(false);
setIbAccess(null); setIbAccess(null);
setIbLimit("");
setMbEnabled(false); setMbEnabled(false);
setMbAccess(null); setMbAccess(null);
setMbLimit("");
}; };
const handleMainAction = () => { const handleMainAction = () => {
if (!isValidated) { if (!isValidated) handleValidate();
handleValidate(); else if (!confirmedPreview) handlePreview();
} else if (!confirmedPreview) { else handleSubmit();
handlePreview();
} else {
handleSubmit();
}
}; };
return ( return (
<> <>
<Title order={4}>User Configuration</Title> <Title order={4}>User Configuration</Title>
@@ -223,16 +267,26 @@ export default function UserConfiguration() {
value={CIF} value={CIF}
onChange={(e) => { onChange={(e) => {
const input = e.currentTarget.value.replace(/\D/g, ""); const input = e.currentTarget.value.replace(/\D/g, "");
if (input.length <= 11) setCIF(input); if (input.length <= 11)
setCIF(input);
setError(null);
}} }}
disabled={isValidated} disabled={isValidated}
withAsterisk withAsterisk
/> />
{error && (
<Center mt="xl">
<Text c="red" fw={500} size="md">
{error}
</Text>
</Center>
)}
{isValidated && ( {isValidated && (
<Tabs defaultValue="details" mt="md" variant="outline"> <Tabs defaultValue="details" mt="md" variant="outline">
<Tabs.List> <Tabs.List>
<Tabs.Tab value="details" leftSection={<IconUser size={12} />}>User Details</Tabs.Tab> <Tabs.Tab value="details" leftSection={<IconUser size={12} />}>User Details</Tabs.Tab>
<Tabs.Tab value="address" leftSection={<IconHome size={12} />}>Address</Tabs.Tab>
<Tabs.Tab value="rights" leftSection={<IconGavel size={12} />}>Rights</Tabs.Tab> <Tabs.Tab value="rights" leftSection={<IconGavel size={12} />}>Rights</Tabs.Tab>
</Tabs.List> </Tabs.List>
@@ -241,24 +295,32 @@ export default function UserConfiguration() {
<Paper withBorder p="md" radius="md"> <Paper withBorder p="md" radius="md">
<Stack> <Stack>
<Group grow> <Group grow>
<Text fw={700} >User ID:</Text> <Text fw={700} >ID:</Text>
<Text c="dimmed">{userDetails?.userId ?? "N/A"}</Text> <Text>{userDetails?.userId ?? "N/A"}</Text>
</Group> </Group>
<Group grow> <Group grow>
<Text fw={700} >User Name:</Text> <Text fw={700} >Branch:</Text>
<Text>{userDetails?.branchNo ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700} >Name:</Text>
<Text>{userDetails?.name ?? "N/A"}</Text> <Text>{userDetails?.name ?? "N/A"}</Text>
</Group> </Group>
<Group grow> <Group grow>
<Text fw={700} >Mobile Number:</Text> <Text fw={700} >Date of Birth:</Text>
<Text c="dimmed">{userDetails?.mobile ?? "N/A"}</Text> <Text>
</Group> {userDetails?.dateOfBirth
<Group grow> ? `${userDetails.dateOfBirth.slice(0, 2)}/${userDetails.dateOfBirth.slice(2, 4)}/${userDetails.dateOfBirth.slice(4)}`
<Text fw={700} >User Address:</Text> : "N/A"}
<Text>{userDetails?.address ?? "N/A"}</Text> </Text>
</Group> </Group>
<Group grow> <Group grow>
<Text fw={700} >Savings Account:</Text> <Text fw={700} >Savings Account:</Text>
<Text c="blue">{savingsAccount ?? "N/A"}</Text> <Text c="blue">
{Array.isArray(savingsAccount)
? savingsAccount.join(", ")
: savingsAccount ?? "N/A"}
</Text>
</Group> </Group>
<Group grow> <Group grow>
<Text fw={700}>Active Accounts:</Text> <Text fw={700}>Active Accounts:</Text>
@@ -269,9 +331,31 @@ export default function UserConfiguration() {
</ScrollArea> </ScrollArea>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="address" pt="xs">
<ScrollArea h={250} type="auto" offsetScrollbars>
<Paper withBorder p="md" radius="md">
<Stack>
<Group grow>
<Text fw={700} >Address:</Text>
<Text>{userDetails?.address ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700} >Pincode:</Text>
<Text>{userDetails?.pinCode ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700} >Mobile Number:</Text>
<Text>{userDetails?.mobile ?? "N/A"}</Text>
</Group>
</Stack>
</Paper>
</ScrollArea>
</Tabs.Panel>
<Tabs.Panel value="rights" pt="xs"> <Tabs.Panel value="rights" pt="xs">
<Stack gap="sm"> <Stack gap="md">
<Group gap="xl" align="center"> {/* Internet Banking Section */}
<Group>
<Box w={150}> <Box w={150}>
<Text fw={700}>Internet Banking</Text> <Text fw={700}>Internet Banking</Text>
</Box> </Box>
@@ -281,7 +365,10 @@ export default function UserConfiguration() {
checked={ibEnabled} checked={ibEnabled}
onChange={(e) => { onChange={(e) => {
setIbEnabled(e.currentTarget.checked); setIbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setIbAccess(null); if (!e.currentTarget.checked) {
setIbAccess(null);
setIbLimit("");
}
}} }}
/> />
</Box> </Box>
@@ -302,8 +389,25 @@ export default function UserConfiguration() {
/> />
</Box> </Box>
</Group> </Group>
<Group align="center" gap="sm">
<Text fw={700}>
Fund Transfer Limit for Internet Banking
</Text>
<TextInput
placeholder="Enter Amount"
value={ibLimit}
onChange={(e) => {
const val = e.currentTarget.value;
if (val === "" || (/^\d{0,7}$/.test(val))) {
setIbLimit(val);
}
}}
/>
</Group>
<Group gap="xl" align="center"> <Divider my="sm" labelPosition="center" />
<Group >
<Box w={150}> <Box w={150}>
<Text fw={700}>Mobile Banking</Text> <Text fw={700}>Mobile Banking</Text>
</Box> </Box>
@@ -313,7 +417,10 @@ export default function UserConfiguration() {
checked={mbEnabled} checked={mbEnabled}
onChange={(e) => { onChange={(e) => {
setMbEnabled(e.currentTarget.checked); setMbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setMbAccess(null); if (!e.currentTarget.checked) {
setMbAccess(null);
setMbLimit("");
}
}} }}
/> />
</Box> </Box>
@@ -334,6 +441,21 @@ export default function UserConfiguration() {
/> />
</Box> </Box>
</Group> </Group>
<Group align="center" gap="sm">
<Text fw={700}>
Fund Transfer Limit for Mobile Banking
</Text>
<TextInput
placeholder="Enter Amount"
value={mbLimit}
onChange={(e) => {
const val = e.currentTarget.value;
if (val === "" || (/^\d{0,7}$/.test(val))) {
setMbLimit(val);
}
}}
/>
</Group>
</Stack> </Stack>
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
@@ -355,9 +477,12 @@ export default function UserConfiguration() {
<Text> <Text>
<strong>Mobile Number:</strong> {userDetails?.mobile ?? "N/A"} <strong>Mobile Number:</strong> {userDetails?.mobile ?? "N/A"}
</Text> </Text>
<Divider label="Rights" size="xs" /> <Divider label="Rights" size="xs" />
{/* Internet Banking Section */}
<TextInput <TextInput
label="Internet Banking" label="Internet Banking Access"
value={ value={
ibAccess === "1" ibAccess === "1"
? "Transaction" ? "Transaction"
@@ -367,8 +492,16 @@ export default function UserConfiguration() {
} }
readOnly readOnly
/> />
<TextInput <TextInput
label="Mobile Banking" label="Internet Banking Limit"
value={ibLimit || "N/A"}
readOnly
/>
{/* Mobile Banking Section */}
<TextInput
label="Mobile Banking Access"
value={ value={
mbAccess === "1" mbAccess === "1"
? "Transaction" ? "Transaction"
@@ -378,7 +511,14 @@ export default function UserConfiguration() {
} }
readOnly readOnly
/> />
<TextInput
label="Mobile Banking Limit"
value={mbLimit || "N/A"}
readOnly
/>
</Stack> </Stack>
<Group justify="right" mt="md"> <Group justify="right" mt="md">
<Button variant="default" onClick={() => setShowPreviewModal(false)}> <Button variant="default" onClick={() => setShowPreviewModal(false)}>
Cancel Cancel

View File

@@ -1,176 +1,200 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
TextInput, TextInput,
Button, Button,
Title, Title,
Stack, Stack,
Group, Group,
Text, Text,
Divider, Divider,
LoadingOverlay, LoadingOverlay,
Box, Box,
Paper, Paper,
Center,
} from "@mantine/core"; } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
interface UserRights { interface UserRights {
customer_no: string; customer_no: string;
created_at: string; created_at: string;
last_login: Date; last_login: Date;
is_first_login: boolean; is_first_login: boolean;
ibAccess: "1" | "2" | "0" | null; ibAccess: "1" | "2" | "0" | null;
mbAccess: "1" | "2" | "0" | null; mbAccess: "1" | "2" | "0" | null;
} }
export default function ViewUserRights() { export default function ViewUserRights() {
const [CIFNo, setCIFNo] = useState(""); const [CIFNo, setCIFNo] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [userData, setUserData] = useState<UserRights | null>(null); const [userData, setUserData] = useState<UserRights | null>(null);
const [detailsExpanded, setDetailsExpanded] = useState(true); const [error, setError] = useState<string | null>(null);
const handleSearch = async () => { const handleSearch = async () => {
if (!CIFNo) { setError(null);
notifications.show({ if (!CIFNo) {
color: "red", notifications.show({
title: "Required", color: "red",
message: "Please enter CIF Number.", title: "CIF Missing",
}); message: "Please enter a CIF number to proceed.",
return; });
} setUserData(null);
setLoading(true); return;
setUserData(null); }
setLoading(true);
setUserData(null);
try {
const token = localStorage.getItem("admin_access_token");
const response = await fetch(`/api/auth/admin/user/rights?CIF=${CIFNo}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "Admin",
Authorization: `Bearer ${token}`,
},
});
try { const data = await response.json();
const token = localStorage.getItem("admin_access_token");
const response = await fetch(
`/api/auth/admin/user/rights?CIF=${CIFNo}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "Admin",
Authorization: `Bearer ${token}`,
},
}
);
const data = await response.json(); if (response.ok && data) {
if (response.ok && data) { setUserData({
setUserData({ customer_no: data.customer_no,
customer_no: data.customer_no, created_at: data.created_at,
created_at: data.created_at, last_login: data.last_login,
last_login: data.last_login, is_first_login: data.is_first_login,
is_first_login: data.is_first_login, ibAccess: data.ib_access_level,
ibAccess: data.ib_access_level, mbAccess: data.mb_access_level,
mbAccess: data.mb_access_level, });
}); } else {
} else { setError("User not present or invalid CIF number.");
notifications.show({ }
color: "red", } catch (err) {
title: "Not Found", setError("Error fetching user rights. Please try again.");
message: "No user rights found for this identifier.", } finally {
}); setLoading(false);
}
};
return (
<Box>
<LoadingOverlay visible={loading} zIndex={1000} />
<Title order={4}>View User Rights</Title>
<Group mt="sm">
<TextInput
placeholder="Enter CIF Number"
value={CIFNo}
onInput={(e) => {
const input = e.currentTarget.value.replace(/\D/g, "");
if (input.length <= 11) {
setCIFNo(input);
setUserData(null);
setError(null);
} }
} catch (err) { }}
notifications.show({ style={{ flex: 1 }}
color: "red", />
title: "Failed", <Button onClick={handleSearch} disabled={loading}>
message: "Error fetching user rights", Search
}); </Button>
} finally { </Group>
setLoading(false);
}
};
return ( {/* Error message in center */}
<Box> {error && (
<LoadingOverlay visible={loading} zIndex={1000} /> <Center mt="xl">
<Title order={4}>View User Rights</Title> <Text c="red" fw={500} size="md">
<Group mt="sm"> {error}
<TextInput </Text>
// label="Enter CIF No" </Center>
placeholder="Enter CIF Number" )}
value={CIFNo}
onInput={(e) => {
const input = e.currentTarget.value.replace(/\D/g, "");
if (input.length <= 11) {
setCIFNo(input);
setUserData(null)
};
}}
style={{ flex: 1 }}
/>
<Button onClick={handleSearch} disabled={loading}>
Search
</Button>
</Group>
{userData && ( {userData && (
<> <>
<Divider size="xs" label="User Info" mt="md" /> <Divider size="xs" label="User Info" mt="md" />
<Paper <Paper
style={{ style={{
border: "1px solid #47C44D", border: "1px solid #47C44D",
borderRadius: 8, borderRadius: 8,
padding: 12, padding: 12,
marginTop: 8, marginTop: 8,
}} }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between"> <Group justify="space-between">
<Text size="sm" fw={500} c="dimmed" w={150}>CIF Number:</Text> <Text size="sm" fw={500} c="dimmed" w={150}>
<Text size="md">{userData.customer_no}</Text> CIF Number:
</Group> </Text>
<Group justify="space-between"> <Text size="md">{userData.customer_no}</Text>
<Text size="sm" fw={500} c="dimmed">User Created :</Text> </Group>
<Text size="md">{new Date(userData.created_at).toLocaleString()}</Text> <Group justify="space-between">
</Group> <Text size="sm" fw={500} c="dimmed">
<Group justify="space-between"> User Created :
<Text size="sm" fw={500} c="dimmed">User Status:</Text> </Text>
<Text size="md" c="green">Active</Text> <Text size="md">
</Group> {new Date(userData.created_at).toLocaleString()}
<Group justify="space-between"> </Text>
<Text size="sm" fw={500} c="dimmed">User Registered Status:</Text> </Group>
<Text size="md" c={userData.is_first_login ? "orange" : "green"}> <Group justify="space-between">
{userData.is_first_login ? "Not Registered" : "Registered"} <Text size="sm" fw={500} c="dimmed">
</Text> User Status:
</Group> </Text>
</Stack> <Text size="md" c="green">
</Paper> Active
<Divider label="Rights" size="xs" mt="md" /> </Text>
<Paper style={{ </Group>
border: "1px solid #47C44D", <Group justify="space-between">
borderRadius: 8, <Text size="sm" fw={500} c="dimmed">
padding: 12, User Registered Status:
marginTop: 8, </Text>
}}> <Text
<Stack gap="sm" mt="sm"> size="md"
<Group justify="space-between"> c={userData.is_first_login ? "orange" : "green"}
<Text size="sm" fw={500} c="dimmed" w={150}>Internet Banking Rights:</Text> >
<Text size="md"> {userData.is_first_login
{userData.ibAccess === "1" ? "Not Registered"
? "Transaction Mode" : "Registered"}
: userData.ibAccess === "2" </Text>
? "Read Mode" </Group>
: "Not Declared"} </Stack>
</Text> </Paper>
</Group>
<Group justify="space-between"> <Divider label="Rights" size="xs" mt="md" />
<Text size="sm" fw={500} c="dimmed" w={150}>Mobile Banking Rights:</Text> <Paper
<Text size="md"> style={{
{userData.mbAccess === "1" border: "1px solid #47C44D",
? "Transaction Mode" borderRadius: 8,
: userData.mbAccess === "2" padding: 12,
? "Read Mode" marginTop: 8,
: "Not Declared"} }}
</Text> >
</Group> <Stack gap="sm" mt="sm">
</Stack> <Group justify="space-between">
</Paper> <Text size="sm" fw={500} c="dimmed" w={150}>
</> Internet Banking Rights:
) </Text>
} <Text size="md">
</Box > {userData.ibAccess === "1"
); ? "Transaction Mode"
: userData.ibAccess === "2"
? "Read Mode"
: "Not Declared"}
</Text>
</Group>
<Group justify="space-between">
<Text size="sm" fw={500} c="dimmed" w={150}>
Mobile Banking Rights:
</Text>
<Text size="md">
{userData.mbAccess === "1"
? "Transaction Mode"
: userData.mbAccess === "2"
? "Read Mode"
: "Not Declared"}
</Text>
</Group>
</Stack>
</Paper>
</>
)}
</Box>
);
} }

View File

@@ -1,183 +1,332 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Text, Box, Image, Button, Stack, Divider, Title } from "@mantine/core"; import {
Text,
Box,
Image,
Stack,
Divider,
Title,
Collapse,
Group,
UnstyledButton,
} from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { Providers } from "@/app/providers"; import { Providers } from "@/app/providers";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import NextImage from "next/image"; import NextImage from "next/image";
import logo from '@/app/image/logo1.jpg'; import logo from "@/app/image/logo1.jpg";
import { IconEye, IconLogout, IconPhoneFilled, IconUsers, IconUserScreen } from "@tabler/icons-react"; import {
IconLogout,
IconPhoneFilled,
IconUsers,
IconChevronDown,
IconChevronUp,
IconSettings,
} from "@tabler/icons-react";
import UserConfiguration from "./UserConfiguration"; import UserConfiguration from "./UserConfiguration";
import ViewUserConfiguration from "./ViewUserConfiguration"; import ViewUserConfiguration from "./ViewUserConfiguration";
import UnlockedUsers from "./UnlockedUsers";
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
const [authorized, SetAuthorized] = useState<boolean | null>(null); const [authorized, SetAuthorized] = useState<boolean | null>(null);
const [view, setView] = useState<'userConf' | 'view' | null>(null); const [view, setView] = useState<string | null>(null);
const [lastLoginDetails, setLastLoginDetails] = useState(null); const [name, setName] = useState<string | null>(null);
const [name, setName] = useState(null); const [lastLoginDetails, setLastLoginDetails] = useState<string | null>(null);
const [userMenuOpen, setUserMenuOpen] = useState(true);
const [configMenuOpen, setConfigMenuOpen] = useState(false);
async function handleLogout(e: React.FormEvent) { async function handleLogout(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
localStorage.removeItem("admin_access_token"); localStorage.clear();
localStorage.clear(); sessionStorage.clear();
sessionStorage.clear(); router.push("/administrator/login");
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",
"X-Login-Type": "Admin",
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
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,
});
} }
async function handleFetchUserDetails(e: React.FormEvent) { }
e.preventDefault();
const token = localStorage.getItem("admin_access_token"); useEffect(() => {
const response = await fetch('/api/auth/admin/admin_details', { const token = localStorage.getItem("admin_access_token");
method: 'GET', if (!token) {
headers: { SetAuthorized(false);
'Content-Type': 'application/json', router.push("/administrator/login/");
"X-Login-Type": "Admin", } else {
'Authorization': `Bearer ${token}` SetAuthorized(true);
}, const fetchLoginTime = async () => {
}); const result = await handleFetchUserDetails({
const data = await response.json(); preventDefault: () => { },
// console.log(data) } as React.FormEvent);
if (response.ok) { if (result) {
return data; setLastLoginDetails(result.last_login);
} setName(result.name);
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,
});
} }
};
fetchLoginTime();
} }
}, []);
useEffect(() => { if (!authorized) return null;
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') => { return (
if (type === 'UserConf') { <Providers>
setView('userConf'); <div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "100%" }}>
} else if (type === 'ViewUser') { {/* Header */}
setView('view'); <Box
} style={{
}; height: "60px",
position: "relative",
width: "100%",
display: "flex",
justifyContent: "flex-start",
background:
"linear-gradient(15deg,rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
}}
>
<Image
fit="cover"
src={logo}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "100%" }}
/>
<Title
order={2}
style={{
fontFamily: "Roboto",
position: "absolute",
top: "30%",
left: "6%",
color: "White",
}}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
<Text
style={{
position: "absolute",
top: "50%",
left: "80%",
color: "white",
textShadow: "1px 1px 2px black",
}}
>
<IconPhoneFilled size={18} /> Toll Free No : 1800-180-8008
</Text>
</Box>
if (!authorized) return null; {/* Layout */}
<Box style={{ display: "flex", height: "90vh" }}>
{/* Sidebar */}
<Box
style={{
width: 240,
background: "#02a355",
padding: "0.75rem",
borderRight: "1px solid #ccc",
color: "white",
fontSize: "13px",
}}
>
<Title order={4} c="white" style={{ textAlign: "center", marginBottom: "6px" }}>
Admin Portal
</Title>
<Divider my="xs" color="rgba(255,255,255,0.3)" />
if (authorized) { {/* User Maintenance */}
return ( <UnstyledButton
<Providers> onClick={() => setUserMenuOpen(!userMenuOpen)}
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "100%", }}> style={{
{/* Header */} display: "flex",
<Box alignItems: "center",
style={{ justifyContent: "space-between",
height: "60px", width: "100%",
position: 'relative', padding: "5px 6px",
width: '100%', borderRadius: "4px",
display: "flex", backgroundColor: "rgba(255,255,255,0.1)",
justifyContent: "flex-start", color: "white",
background: "linear-gradient(15deg,rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)", fontSize: "13px",
}} }}
> >
<Image <Group gap="xs">
fit="cover" <IconUsers size={15} />
src={logo} <Text fw={600} size="sm">
component={NextImage} User Maintenance
alt="ebanking" </Text>
style={{ width: "100%", height: "100%" }} </Group>
/> {userMenuOpen ? <IconChevronUp size={15} /> : <IconChevronDown size={15} />}
<Title </UnstyledButton>
order={2} <Collapse in={userMenuOpen}>
style={{ <Stack gap={2} pl="md" mt={4}>
fontFamily: 'Roboto', <Box
position: 'absolute', onClick={() => setView("userConf")}
top: '30%', style={{
left: '6%', cursor: "pointer",
color: 'White', color: view === "userConf" ? "#02a355" : "white",
transition: "opacity 0.5s ease-in-out", backgroundColor: view === "userConf" ? "white" : "transparent",
}} borderRadius: "3px",
> padding: "3px 6px",
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD. transition: "0.2s",
</Title> }}
<Text >
style={{ User Create / Update
position: 'absolute', </Box>
top: '50%', <Box
left: '80%', onClick={() => setView("viewUser")}
color: 'white', style={{
textShadow: '1px 1px 2px black', cursor: "pointer",
}} color: view === "viewUser" ? "#02a355" : "white",
> backgroundColor: view === "viewUser" ? "white" : "transparent",
<IconPhoneFilled size={20} /> Toll Free No : 1800-180-8008 borderRadius: "3px",
</Text> padding: "3px 6px",
</Box> transition: "0.2s",
{/* layout and */} }}
<Box style={{ display: 'flex', height: '90vh' }}> >
{/* Sidebar manually placed under header */} Users Status
<Box </Box>
style={{ <Box
width: 250, onClick={() => setView("unlockUser")}
background: "#ebf5f5ff", style={{
// background: "linear-gradient(90deg,rgba(42, 123, 155, 1) 0%, rgba(87, 199, 133, 1) 30%)", cursor: "pointer",
padding: '1rem', color: view === "unlockUser" ? "#02a355" : "white",
borderRight: '1px solid #ccc', backgroundColor: view === "unlockUser" ? "white" : "transparent",
}} borderRadius: "3px",
> padding: "3px 6px",
<Title order={3} style={{ textAlign: 'center' }}>Admin Portal</Title> transition: "0.2s",
<Divider my="xs" label="Menu" labelPosition="center" /> }}
<Stack> >
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('UserConf')}> User Unlock
<IconUsers size="15" /> User Configuration </Box>
</Text> </Stack>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('ViewUser')}> </Collapse>
<IconEye size="15" /> View User Configuration
</Text>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={handleLogout}>
<IconLogout size="15" /> Logout
</Text>
</Stack>
</Box>
{/* Main Content Area */} {/* Configuration */}
<Stack style={{ flex: 1, padding: '1rem' }}> <UnstyledButton
<Box> onClick={() => setConfigMenuOpen(!configMenuOpen)}
<Text c="Blue" size="lg" fs='italic'>Welcome ,{name} </Text> style={{
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: '13px' }}> display: "flex",
Last logged in at {lastLoginDetails ? new Date(lastLoginDetails).toLocaleString() : "N/A"} alignItems: "center",
</Text> justifyContent: "space-between",
</Box> width: "100%",
<Box> padding: "5px 6px",
{view === 'userConf' && <UserConfiguration />} marginTop: "8px",
{view === 'view' && <Text size="xl"><ViewUserConfiguration /></Text>} borderRadius: "4px",
{!view && backgroundColor: "rgba(255,255,255,0.1)",
<Text size="xl">Welcome To The Admin Portal</Text> color: "white",
} fontSize: "13px",
</Box> }}
</Stack> >
</Box> <Group gap="xs">
<Divider size="xs" color='#99c2ff' /> <IconSettings size={15} />
<Text size="sm" style={{ textAlign: "center" }}>© 2025 The Kangra Central Co-Operative Bank</Text> <Text fw={600} size="sm">
</div> Configuration
</Providers> </Text>
); </Group>
} {configMenuOpen ? <IconChevronUp size={15} /> : <IconChevronDown size={15} />}
</UnstyledButton>
<Collapse in={configMenuOpen}>
<Stack gap={2} pl="md" mt={4}>
<Box
onClick={() => setView("ifscConfig")}
style={{
cursor: "pointer",
color: view === "ifscConfig" ? "#02a355" : "white",
backgroundColor: view === "ifscConfig" ? "white" : "transparent",
borderRadius: "3px",
padding: "3px 6px",
transition: "0.2s",
}}
>
IFSC Configuration
</Box>
</Stack>
</Collapse>
<Divider my="xs" color="rgba(255,255,255,0.3)" />
<Text
onClick={handleLogout}
style={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "4px",
color: "white",
marginTop: "8px",
fontSize: "13px",
}}
>
<IconLogout size={14} /> Logout
</Text>
</Box>
{/* Main Content */}
<Stack style={{ flex: 1, padding: "1rem" }}>
<Box>
<Text c="Blue" size="sm" fs="italic">
Welcome, {name}
</Text>
<Text size="xs" c="gray" style={{ fontFamily: "inter" }}>
Last logged in at{" "}
{lastLoginDetails
? new Date(lastLoginDetails).toLocaleString()
: "N/A"}
</Text>
</Box>
<Box>
{view === "userConf" && <UserConfiguration />}
{view === "viewUser" && <ViewUserConfiguration />}
{view === "unlockUser" && <UnlockedUsers />}
{view === "ifscConfig" && (
<Text size="sm" c="gray">
IFSC Configuration Page Coming Soon
</Text>
)}
{!view && (
<>
<Text size="xl" c="gray">
Welcome To The Internet Banking Admin Portal
</Text>
<Text size="sm" c="black">
Choose the service from the menu.
</Text>
</>
)}
</Box>
</Stack>
</Box>
<Divider size="xs" color="#99c2ff" />
<Text size="xs" style={{ textAlign: "center" }}>
© 2025 The Kangra Central Co-Operative Bank
</Text>
</div>
</Providers>
);
} }

View File

@@ -45,8 +45,8 @@ export default function Login() {
} }
try { try {
await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: mobile }); // await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: mobile });
// await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: "7890544527" }); await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: "7890544527" });
notifications.show({ notifications.show({
color: 'orange', color: 'orange',
title: 'OTP Required', title: 'OTP Required',
@@ -67,8 +67,8 @@ export default function Login() {
async function handleVerifyOtp(mobile?: string) { async function handleVerifyOtp(mobile?: string) {
try { try {
if (mobile) { if (mobile) {
await verifyLoginOtp(otp, mobile); // await verifyLoginOtp(otp, mobile);
// await verifyLoginOtp(otp, '7890544527'); await verifyLoginOtp(otp, '7890544527');
return true; return true;
} }
} }