feat : update rights from admin user.

This commit is contained in:
2025-09-12 15:35:52 +05:30
parent 3a006bf3c1
commit b7a454f53b
3 changed files with 354 additions and 344 deletions

View File

@@ -62,7 +62,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
if (response.ok && Array.isArray(data)) { if (response.ok && Array.isArray(data)) {
if (data.length > 0) { if (data.length > 0) {
const name = data[0].custname; const name = data[0].custname;
const mobileNumber = data[0].mobileno;
localStorage.setItem("remitter_name", name); localStorage.setItem("remitter_name", name);
localStorage.setItem("remitter_mobile_no", mobileNumber);
setCustname(name); setCustname(name);
} }
} else { } else {
@@ -249,7 +251,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Popover <Popover
opened={opened} opened={opened}
onChange={close} onChange={close}
position="bottom-end" // 👈 Logout button ke niche right align position="bottom-end"
withArrow withArrow
shadow="md" shadow="md"
> >
@@ -277,11 +279,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</Group> </Group>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
</Group> </Group>
</div> </div>

View File

@@ -1,79 +1,91 @@
import React, { useState } from 'react'; "use client";
import { TextInput, Button, Title, Stack, Group, Text, Divider, LoadingOverlay, Box, Modal, Checkbox } from '@mantine/core'; import React, { useState } from "react";
import { notifications } from '@mantine/notifications'; import {
TextInput,
Button,
Title,
Stack,
Group,
Text,
Divider,
LoadingOverlay,
Box,
Modal,
Checkbox,
Paper,
Tabs,
ScrollArea,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { IconGavel, IconUser } from "@tabler/icons-react";
export default function UserConfiguration() { export default function UserConfiguration() {
const router = useRouter(); const router = useRouter();
const [CIF, setCIF] = useState(''); const [CIF, setCIF] = useState("");
const [userDetails, setUserDetails] = useState<any>(null); const [userDetails, setUserDetails] = useState<any>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [savingsAccount, setSavingsAccount] = useState<string | null>(null); const [savingsAccount, setSavingsAccount] = useState<string | null>(null);
const [accountError, setAccountError] = useState<string>('');
const [detailsExpanded, setDetailsExpanded] = useState(true);
const [showPreviewModal, setShowPreviewModal] = useState(false); const [showPreviewModal, setShowPreviewModal] = useState(false);
const [confirmedPreview, setConfirmedPreview] = useState(false); const [confirmedPreview, setConfirmedPreview] = useState(false);
const [ibEnabled, setIbEnabled] = useState(false); const [ibEnabled, setIbEnabled] = useState(false);
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 isValidated = !!userDetails; const isValidated = !!userDetails;
const canSubmit = isValidated && !!savingsAccount && confirmedPreview; const canSubmit =
isValidated && savingsAccount && userDetails?.mobile && confirmedPreview;
const handleValidate = async () => { const handleValidate = async () => {
if (!CIF) { if (!CIF) {
notifications.show({ notifications.show({
color: 'red', color: "red",
title: 'CIF Missing', title: "CIF Missing",
message: 'Please enter a CIF number to proceed.' message: "Please enter a CIF number to proceed.",
}); });
return; return;
} }
setLoading(true); setLoading(true);
setAccountError('');
setSavingsAccount(null);
setUserDetails(null);
try { try {
const token = localStorage.getItem("admin_access_token"); const token = localStorage.getItem("admin_access_token");
const response = await fetch(`/api/auth/admin/fetch/customer_details?CIF=${CIF}`, { const response = await fetch(
method: 'GET', `/api/auth/admin/fetch/customer_details?CIF=${CIF}`,
headers: { {
'Content-Type': 'application/json', method: "GET",
'Authorization': `Bearer ${token}` headers: {
}, "Content-Type": "application/json",
}); Authorization: `Bearer ${token}`,
},
}
);
const data = await response.json(); const data = await response.json();
if (response.ok && Array.isArray(data) && data.length > 0) { if (response.ok && Array.isArray(data) && data.length > 0) {
const saAccount = data.find(acc => acc.stAccountType === 'SA'); const saAccount = data.find((acc) => acc.stAccountType === "SA");
// console.log(saAccount); setSavingsAccount(saAccount?.stAccountNo ?? null);
if (saAccount) {
setSavingsAccount(saAccount.stAccountNo);
} else {
setAccountError('At least one savings account is required for requesting.');
}
const user = data[0]; const user = data[0];
setUserDetails({ setUserDetails({
name: user.custname, name: user.custname,
userId: user.id, userId: user.id,
mobile: user.mobileno, mobile: user.mobileno ?? null,
address: user.custaddress, address: user.custaddress,
activeAccount: user.activeAccounts, activeAccount: user.activeAccounts,
}); });
if (!user.mobileno) {
localStorage.setItem("user_mob_no", user.mobileno);
setAccountError('User not have any registered Mobile Number');
}
} else { } else {
throw new Error('User not found or data format incorrect'); notifications.show({
color: "red",
title: "Validation Failed",
message: "User not found",
});
} }
} catch (err: any) { } catch (err) {
console.error(err);
notifications.show({ notifications.show({
color: 'red', color: "red",
title: 'Validation Failed', title: "Validation Failed",
message: 'User not found', message: "User not found",
}); });
} finally { } finally {
setLoading(false); setLoading(false);
@@ -81,22 +93,14 @@ export default function UserConfiguration() {
}; };
const handlePreview = () => { const handlePreview = () => {
const hasRight = ibAccess || mbAccess; if (!savingsAccount || !userDetails?.mobile) {
const hasSavings = savingsAccount;
if (!hasRight) {
notifications.show({
title: "Rights Required",
message: "Please select at least one rights (Internet Banking or Mobile Banking).",
color: "red",
});
return; return;
} }
if (!ibAccess && !mbAccess) {
if (!hasSavings) {
notifications.show({ notifications.show({
title: "Savings Account Required", title: "Rights Required",
message: "User must have at least one Savings account.", message:
"Please select at least one right (Internet Banking or Mobile Banking).",
color: "red", color: "red",
}); });
return; return;
@@ -105,302 +109,310 @@ export default function UserConfiguration() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!canSubmit) { if (!canSubmit) return;
notifications.show({
color: 'red',
title: 'Required',
message: 'One savings account is required for enable rights',
});
return;
}
else {
try {
const token = localStorage.getItem("admin_access_token");
const response = await fetch('/api/auth/admin/user/rights', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
CIF: CIF,
ib_access_level: ibAccess,
mb_access_level: mbAccess
}),
});
const data = await response.json();
if (response.ok) {
if (data?.otp) {
const otp = data.otp;
try {
const otp_response = await fetch('/api/otp/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mobileNumber: localStorage.getItem("user_mob_no"),
type: "REGISTRATION",
userOtp: otp
}),
});
// const otp_result = await otp_response.json();
if (otp_response.ok) {
notifications.show({
color: 'blue',
title: 'Submitted',
message: `User ${CIF} rights submitted successfully. The password has been sent to the user as an OTP.`,
});
}
}
catch (err: any) {
notifications.show({
color: 'red',
title: 'Failed to Send',
message: `Rights for User ${CIF} saved successfully. Message delivery failed.`,
autoClose: false,
});
}
}
if(data?.message){
try {
const otp_response = await fetch('/api/otp/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mobileNumber: localStorage.getItem("user_mob_no"),
type: "RIGHT_UPDATE",
}),
});
if (otp_response.ok) {
notifications.show({
color: 'blue',
title: 'Submitted',
message: `User ${CIF} rights updated successfully. The confirmation has been sent to the user.`,
});
}
}
catch (err: any) {
notifications.show({
color: 'red',
title: 'Failed to Send',
message: `Rights for User ${CIF} saved successfully. Message delivery failed.`,
autoClose: false,
});
}
}
}
else if (response.status === 401 || data.message === 'invalid or expired token') {
localStorage.removeItem("admin_access_token");
localStorage.clear();
sessionStorage.clear();
router.push('/administrator/login');
}
}
catch (err: any) {
notifications.show({
color: 'red',
title: 'Failed -Right',
message: `Can not Processed user -${CIF} rights.`,
autoClose: false,
});
try {
const token = localStorage.getItem("admin_access_token");
const response = await fetch("/api/auth/admin/user/rights", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
CIF,
ib_access_level: ibAccess,
mb_access_level: mbAccess,
}),
});
const data = await response.json();
if (response.ok) {
if (data?.otp) {
// Registration case
await fetch('/api/otp/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mobileNumber: userDetails.mobile,
type: 'REGISTRATION',
userOtp: data.otp,
}),
});
notifications.show({
color: 'blue',
title: 'Submitted',
message: `User ${CIF} rights submitted successfully. OTP sent to the user.`,
});
}
else if (data?.message) {
// Rights update case
await fetch('/api/otp/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mobileNumber: userDetails.mobile,
type: 'RIGHT_UPDATE',
}),
});
notifications.show({
color: 'blue',
title: 'Submitted',
message: `User ${CIF} rights updated successfully. Confirmation sent to the user.`,
});
}
} }
finally { else if (response.status === 401 || data.message === "invalid or expired token") {
setSavingsAccount(null); localStorage.clear();
setUserDetails(null); sessionStorage.clear();
setCIF(''); router.push("/administrator/login");
setConfirmedPreview(false);
} }
}; }
} catch (err) {
console.error(err);
notifications.show({
color: "red",
title: "Failed - Right",
message: `Cannot process user ${CIF} rights.`,
autoClose: false,
});
} finally {
handleReset();
}
};
const handleReset = () => {
setSavingsAccount(null);
setUserDetails(null);
setCIF("");
setConfirmedPreview(false);
setIbEnabled(false);
setIbAccess(null);
setMbEnabled(false);
setMbAccess(null);
};
const handleMainAction = () => { const handleMainAction = () => {
if (!isValidated) { if (!isValidated) {
handleValidate(); handleValidate();
} else if (!confirmedPreview) { } else if (!confirmedPreview) {
handlePreview(); handlePreview();
} else if (isValidated && confirmedPreview) { } else {
handleSubmit(); handleSubmit();
} }
}; };
return ( return (
<Box> <>
<LoadingOverlay visible={loading} zIndex={1000} />
<Title order={4}>User Configuration</Title> <Title order={4}>User Configuration</Title>
<TextInput
label="Enter CIF No" <Box
placeholder="Enter your CIF No" style={{
value={CIF} height: "65vh",
onChange={(e) => { maxHeight: "65vh",
const input = e.currentTarget.value.replace(/\D/g, ''); // overflowY: "auto",
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 gap="sm">
{/* Header row */}
<Group gap="xl">
<Box w={150}>
<Text fw={500}>Services</Text>
</Box>
<Box w={100}>
<Text fw={500}>Enable</Text>
</Box>
<Box w={120}>
<Text fw={500}>Transaction</Text>
</Box>
<Box w={100}>
<Text fw={500}>Read</Text>
</Box>
</Group>
{/* Internet Banking row */}
<Group gap="xl" align="center">
<Box w={150}>
<Text>Internet Banking</Text>
</Box>
<Box w={100}>
<Checkbox
disabled={isValidated && confirmedPreview}
checked={ibEnabled}
onChange={(e) => {
setIbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setIbAccess(null);
}}
/>
</Box>
<Box w={120}>
<Checkbox
disabled={!ibEnabled || isValidated && confirmedPreview}
checked={ibAccess === "1"}
onChange={() => setIbAccess("1")}
/>
</Box>
<Box w={100}>
<Checkbox
disabled={!ibEnabled || isValidated && confirmedPreview}
checked={ibAccess === "2"}
onChange={() => setIbAccess("2")}
/>
</Box>
</Group>
{/* Mobile Banking row */}
<Group gap="xl" align="center">
<Box w={150}>
<Text>Mobile Banking</Text>
</Box>
<Box w={100}>
<Checkbox
disabled={isValidated && confirmedPreview}
checked={mbEnabled}
onChange={(e) => {
setMbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setMbAccess(null);
}}
/>
</Box>
<Box w={120}>
<Checkbox
disabled={!mbEnabled || isValidated && confirmedPreview}
checked={mbAccess === "1"}
onChange={() => setMbAccess("1")}
/>
</Box>
<Box w={100}>
<Checkbox
disabled={!mbEnabled || isValidated && confirmedPreview}
checked={mbAccess === "2"}
onChange={() => setMbAccess("2")}
/>
</Box>
</Group>
</Stack>
{accountError && (
<Text c="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> <LoadingOverlay visible={loading} zIndex={1000} />
<Text><strong>CIF:</strong> {CIF}</Text>
<Text><strong>Savings Account:</strong> {savingsAccount}</Text>
<Text><strong>Mobile Number:</strong> {userDetails?.mobile ?? "N/A"}</Text>
<Divider label='Rights' size="xs" />
<TextInput
label="Internet Banking"
value={
ibAccess === "1"
? "Transaction"
: ibAccess === "2"
? "Read"
: "No Rights"
}
readOnly
disabled
/>
<TextInput
label="Mobile Banking"
value={
mbAccess === "1"
? "Transaction"
: mbAccess === "2"
? "Read"
: "No Rights"
}
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>
<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 && (
<Tabs defaultValue="details" mt="md" variant="outline">
<Tabs.List>
<Tabs.Tab value="details" leftSection={<IconUser size={12} />}>User Details</Tabs.Tab>
<Tabs.Tab value="rights" leftSection={<IconGavel size={12} />}>Rights</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="details" pt="xs">
<ScrollArea h={250} type="auto" offsetScrollbars>
<Paper withBorder p="md" radius="md">
<Stack>
<Group grow>
<Text fw={700} >User ID:</Text>
<Text c="dimmed">{userDetails?.userId ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700} >User Name:</Text>
<Text>{userDetails?.name ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700} >Mobile Number:</Text>
<Text c="dimmed">{userDetails?.mobile ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700} >User Address:</Text>
<Text>{userDetails?.address ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700} >Savings Account:</Text>
<Text c="blue">{savingsAccount ?? "N/A"}</Text>
</Group>
<Group grow>
<Text fw={700}>Active Accounts:</Text>
<Text c="Green">{userDetails?.activeAccount ?? "N/A"}</Text>
</Group>
</Stack>
</Paper>
</ScrollArea>
</Tabs.Panel>
<Tabs.Panel value="rights" pt="xs">
<Stack gap="sm">
<Group gap="xl" align="center">
<Box w={150}>
<Text fw={700}>Internet Banking</Text>
</Box>
<Box w={100}>
<Checkbox
disabled={confirmedPreview}
checked={ibEnabled}
onChange={(e) => {
setIbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setIbAccess(null);
}}
/>
</Box>
<Box w={120}>
<Checkbox
disabled={!ibEnabled || confirmedPreview}
checked={ibAccess === "1"}
onChange={() => setIbAccess("1")}
label="Transaction"
/>
</Box>
<Box w={100}>
<Checkbox
disabled={!ibEnabled || confirmedPreview}
checked={ibAccess === "2"}
onChange={() => setIbAccess("2")}
label="Read"
/>
</Box>
</Group>
<Group gap="xl" align="center">
<Box w={150}>
<Text fw={700}>Mobile Banking</Text>
</Box>
<Box w={100}>
<Checkbox
disabled={confirmedPreview}
checked={mbEnabled}
onChange={(e) => {
setMbEnabled(e.currentTarget.checked);
if (!e.currentTarget.checked) setMbAccess(null);
}}
/>
</Box>
<Box w={120}>
<Checkbox
disabled={!mbEnabled || confirmedPreview}
checked={mbAccess === "1"}
onChange={() => setMbAccess("1")}
label="Transaction"
/>
</Box>
<Box w={100}>
<Checkbox
disabled={!mbEnabled || confirmedPreview}
checked={mbAccess === "2"}
onChange={() => setMbAccess("2")}
label="Read"
/>
</Box>
</Group>
</Stack>
</Tabs.Panel>
</Tabs>
)}
<Modal
opened={showPreviewModal}
onClose={() => setShowPreviewModal(false)}
title="Confirm User Rights"
centered
>
<Stack>
<Text>
<strong>CIF:</strong> {CIF}
</Text>
<Text>
<strong>Savings Account:</strong> {savingsAccount ?? "N/A"}
</Text>
<Text>
<strong>Mobile Number:</strong> {userDetails?.mobile ?? "N/A"}
</Text>
<Divider label="Rights" size="xs" />
<TextInput
label="Internet Banking"
value={
ibAccess === "1"
? "Transaction"
: ibAccess === "2"
? "Read"
: "No Rights"
}
readOnly
/>
<TextInput
label="Mobile Banking"
value={
mbAccess === "1"
? "Transaction"
: mbAccess === "2"
? "Read"
: "No Rights"
}
readOnly
/>
</Stack>
<Group justify="right" mt="md">
<Button variant="default" onClick={() => setShowPreviewModal(false)}>
Cancel
</Button>
<Button
onClick={() => {
setConfirmedPreview(true);
setShowPreviewModal(false);
}}
disabled={!savingsAccount || !userDetails?.mobile}
>
OK
</Button>
</Group>
</Modal>
</Box>
<Group>
<Button
onClick={handleMainAction}
disabled={
loading ||
(!isValidated && !CIF) ||
(isValidated &&
!confirmedPreview &&
(!savingsAccount || !userDetails?.mobile))
}
>
{!isValidated ? "Validate" : !confirmedPreview ? "Preview" : "Submit"}
</Button>
{isValidated && (
<Button color="red" variant="light" onClick={handleReset}>
Reset
</Button>
)}
</Group>
</>
); );
} }

View File

@@ -145,19 +145,19 @@ export default function Login() {
<Divider my="xs" label="Menu" labelPosition="center" /> <Divider my="xs" label="Menu" labelPosition="center" />
<Stack> <Stack>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('UserConf')}> <Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('UserConf')}>
<IconUsers size ="15"/> User Configuration <IconUsers size="15" /> User Configuration
</Text> </Text>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('ViewUser')}> <Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={() => handleClick('ViewUser')}>
<IconEye size ="15"/> View User Configuration <IconEye size="15" /> View User Configuration
</Text> </Text>
<Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={handleLogout}> <Text td="underline" variant="light" style={{ cursor: 'pointer' }} onClick={handleLogout}>
<IconLogout size ="15"/> Logout <IconLogout size="15" /> Logout
</Text> </Text>
</Stack> </Stack>
</Box> </Box>
{/* Main Content Area */} {/* Main Content Area */}
<Stack style={{ flex: 1, padding: '1rem'}}> <Stack style={{ flex: 1, padding: '1rem' }}>
<Box> <Box>
<Text c="Blue" size="lg" fs='italic'>Welcome ,{name} </Text> <Text c="Blue" size="lg" fs='italic'>Welcome ,{name} </Text>
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: '13px' }}> <Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: '13px' }}>
@@ -166,14 +166,15 @@ export default function Login() {
</Box> </Box>
<Box> <Box>
{view === 'userConf' && <UserConfiguration />} {view === 'userConf' && <UserConfiguration />}
{view === 'view' && <Text size="xl"><ViewUserConfiguration/></Text>} {view === 'view' && <Text size="xl"><ViewUserConfiguration /></Text>}
{!view && {!view &&
<Text size="xl">Welcome To The Admin Portal</Text> <Text size="xl">Welcome To The Admin Portal</Text>
} }
</Box> </Box>
</Stack> </Stack>
</Box> </Box>
<Text size="sm" style={{textAlign:"center"}}>© 2025 The Kangra Central Co-Operative Bank</Text> <Divider size="xs" color='#99c2ff' />
<Text size="sm" style={{ textAlign: "center" }}>© 2025 The Kangra Central Co-Operative Bank</Text>
</div> </div>
</Providers> </Providers>
); );