User suspension added

This commit is contained in:
Sidharth Prabhu
2026-04-21 14:37:34 +05:30
parent 21c581b7eb
commit b4bf6ebeb3
13 changed files with 301 additions and 24 deletions

View File

@@ -14,7 +14,7 @@ public class WebConfig {
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOriginPatterns("*") .allowedOriginPatterns("http://*", "https://*", "file://*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("*") .allowedHeaders("*")
.allowCredentials(true); .allowCredentials(true);

View File

@@ -138,4 +138,18 @@ public class UserController {
userService.deleteUser(id); userService.deleteUser(id);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
/**
* Toggle user suspension status.
* PATCH /api/auth/users/{id}/suspend
*/
@PatchMapping("/users/{id}/suspend")
public ResponseEntity<LoginResponse.UserDto> toggleSuspension(@PathVariable Long id) {
LoginResponse.UserDto updated = userService.toggleSuspension(id);
if (updated != null) {
return ResponseEntity.ok(updated);
} else {
return ResponseEntity.notFound().build();
}
}
} }

View File

@@ -8,10 +8,11 @@ public class DashboardStats {
private double growth; private double growth;
private long totalExpenses; private long totalExpenses;
private long periodExpenses; private long periodExpenses;
private long suspendedUserCount;
public DashboardStats() {} public DashboardStats() {}
public DashboardStats(long totalSales, long periodRevenue, int activeOrders, int dailyCustomers, double growth, long totalExpenses, long periodExpenses) { public DashboardStats(long totalSales, long periodRevenue, int activeOrders, int dailyCustomers, double growth, long totalExpenses, long periodExpenses, long suspendedUserCount) {
this.totalSales = totalSales; this.totalSales = totalSales;
this.periodRevenue = periodRevenue; this.periodRevenue = periodRevenue;
this.activeOrders = activeOrders; this.activeOrders = activeOrders;
@@ -19,6 +20,7 @@ public class DashboardStats {
this.growth = growth; this.growth = growth;
this.totalExpenses = totalExpenses; this.totalExpenses = totalExpenses;
this.periodExpenses = periodExpenses; this.periodExpenses = periodExpenses;
this.suspendedUserCount = suspendedUserCount;
} }
public long getTotalSales() { return totalSales; } public long getTotalSales() { return totalSales; }
@@ -41,4 +43,7 @@ public class DashboardStats {
public long getPeriodExpenses() { return periodExpenses; } public long getPeriodExpenses() { return periodExpenses; }
public void setPeriodExpenses(long periodExpenses) { this.periodExpenses = periodExpenses; } public void setPeriodExpenses(long periodExpenses) { this.periodExpenses = periodExpenses; }
public long getSuspendedUserCount() { return suspendedUserCount; }
public void setSuspendedUserCount(long suspendedUserCount) { this.suspendedUserCount = suspendedUserCount; }
} }

View File

@@ -43,15 +43,17 @@ public class LoginResponse {
private String mobileNumber; private String mobileNumber;
private String name; private String name;
private boolean isLoggedIn; private boolean isLoggedIn;
private boolean isSuspended;
private java.math.BigDecimal ritzTokenBalance; private java.math.BigDecimal ritzTokenBalance;
public UserDto() {} public UserDto() {}
public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn, java.math.BigDecimal ritzTokenBalance) { public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn, boolean isSuspended, java.math.BigDecimal ritzTokenBalance) {
this.id = id; this.id = id;
this.mobileNumber = mobileNumber; this.mobileNumber = mobileNumber;
this.name = name; this.name = name;
this.isLoggedIn = isLoggedIn; this.isLoggedIn = isLoggedIn;
this.isSuspended = isSuspended;
this.ritzTokenBalance = ritzTokenBalance; this.ritzTokenBalance = ritzTokenBalance;
} }
@@ -67,6 +69,9 @@ public class LoginResponse {
public boolean isLoggedIn() { return isLoggedIn; } public boolean isLoggedIn() { return isLoggedIn; }
public void setLoggedIn(boolean loggedIn) { this.isLoggedIn = loggedIn; } public void setLoggedIn(boolean loggedIn) { this.isLoggedIn = loggedIn; }
public boolean isSuspended() { return isSuspended; }
public void setSuspended(boolean suspended) { isSuspended = suspended; }
public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; } public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; }
public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; } public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; }
} }

View File

@@ -37,6 +37,9 @@ public class User {
@Column(nullable = false) @Column(nullable = false)
private java.math.BigDecimal ritzTokenBalance = java.math.BigDecimal.ZERO; private java.math.BigDecimal ritzTokenBalance = java.math.BigDecimal.ZERO;
@Column(nullable = false, columnDefinition = "boolean default false")
private boolean isSuspended = false;
@Column(nullable = true) @Column(nullable = true)
private LocalDateTime lastLoginAt; private LocalDateTime lastLoginAt;
@@ -54,6 +57,9 @@ public class User {
public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; } public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; }
public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; } public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; }
public boolean isSuspended() { return isSuspended; }
public void setSuspended(boolean suspended) { isSuspended = suspended; }
public String getPinHash() { return pinHash; } public String getPinHash() { return pinHash; }
public void setPinHash(String pinHash) { this.pinHash = pinHash; } public void setPinHash(String pinHash) { this.pinHash = pinHash; }

View File

@@ -19,4 +19,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
@org.springframework.data.jpa.repository.Lock(jakarta.persistence.LockModeType.PESSIMISTIC_WRITE) @org.springframework.data.jpa.repository.Lock(jakarta.persistence.LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id") @Query("SELECT u FROM User u WHERE u.id = :id")
Optional<User> findByIdWithLock(@org.springframework.data.repository.query.Param("id") Long id); Optional<User> findByIdWithLock(@org.springframework.data.repository.query.Param("id") Long id);
long countByIsSuspended(boolean isSuspended);
} }

View File

@@ -61,7 +61,7 @@ public class UserService {
userRepository.save(user); userRepository.save(user);
LoginResponse.UserDto userDto = new LoginResponse.UserDto( LoginResponse.UserDto userDto = new LoginResponse.UserDto(
user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance() user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance()
); );
return new LoginResponse(true, "Registration successful. You are now logged in.", userDto); return new LoginResponse(true, "Registration successful. You are now logged in.", userDto);
@@ -79,6 +79,10 @@ public class UserService {
User user = userOpt.get(); User user = userOpt.get();
if (user.isSuspended()) {
return new LoginResponse(false, "Your account has been suspended. Please contact the administrator.");
}
if (!passwordEncoder.matches(pin, user.getPinHash())) { if (!passwordEncoder.matches(pin, user.getPinHash())) {
return new LoginResponse(false, "Incorrect PIN. Please try again."); return new LoginResponse(false, "Incorrect PIN. Please try again.");
} }
@@ -88,7 +92,7 @@ public class UserService {
userRepository.save(user); userRepository.save(user);
LoginResponse.UserDto userDto = new LoginResponse.UserDto( LoginResponse.UserDto userDto = new LoginResponse.UserDto(
user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance() user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance()
); );
return new LoginResponse(true, "Login successful.", userDto); return new LoginResponse(true, "Login successful.", userDto);
@@ -154,7 +158,7 @@ public class UserService {
return null; return null;
} }
User user = userOpt.get(); User user = userOpt.get();
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()); return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance());
} }
/** /**
@@ -173,6 +177,7 @@ public class UserService {
user.getMobileNumber(), user.getMobileNumber(),
user.getName(), user.getName(),
user.isLoggedIn(), user.isLoggedIn(),
user.isSuspended(),
user.getRitzTokenBalance() user.getRitzTokenBalance()
)); ));
} }
@@ -212,7 +217,28 @@ public class UserService {
} }
userRepository.save(user); userRepository.save(user);
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()); return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance());
}
/**
* Toggle the suspension status of a user.
*/
public LoginResponse.UserDto toggleSuspension(Long userId) {
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
return null;
}
User user = userOpt.get();
user.setSuspended(!user.isSuspended());
// If suspended, forcefully log them out
if (user.isSuspended()) {
user.setLoggedIn(false);
}
userRepository.save(user);
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance());
} }
/** /**

View File

@@ -7,6 +7,7 @@ interface UserDto {
mobileNumber: string; mobileNumber: string;
name: string; name: string;
loggedIn: boolean; loggedIn: boolean;
isSuspended: boolean;
ritzTokenBalance: number; ritzTokenBalance: number;
} }
@@ -66,7 +67,13 @@ const Customers: React.FC = () => {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data && data.content) { if (data && data.content) {
setUsers(data.content); // Normalize the data (backend sends 'suspended', frontend expects 'isSuspended')
const normalizedUsers = data.content.map((u: any) => ({
...u,
isSuspended: u.isSuspended !== undefined ? u.isSuspended : u.suspended,
loggedIn: u.loggedIn !== undefined ? u.loggedIn : u.isLoggedIn
}));
setUsers(normalizedUsers);
setTotalElements(data.totalElements); setTotalElements(data.totalElements);
} else { } else {
setUsers([]); setUsers([]);
@@ -115,6 +122,25 @@ const Customers: React.FC = () => {
} }
}; };
const handleSuspendToggle = async (user: UserDto) => {
try {
const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/auth/users/${user.id}/suspend`, {
method: 'PATCH',
});
if (response.ok) {
showNotification(`${user.name} ${user.isSuspended ? 'unsuspended' : 'suspended'} successfully`, 'success');
fetchUsers();
setOpenMenuId(null);
} else {
showNotification('Failed to update suspension status', 'error');
}
} catch (error) {
console.error('Error toggling suspension:', error);
showNotification('Error connecting to server', 'error');
}
};
const validateForm = () => { const validateForm = () => {
const newErrors: {[key: string]: string} = {}; const newErrors: {[key: string]: string} = {};
if (!editForm.name.trim()) newErrors.name = 'Full Name is required'; if (!editForm.name.trim()) newErrors.name = 'Full Name is required';
@@ -283,19 +309,16 @@ const Customers: React.FC = () => {
</span> </span>
</div> </div>
</td> </td>
<td className="px-6 py-4">
<span className="text-sm font-bold text-[#231651]">
{user.ritzTokenBalance?.toLocaleString() || '0'}
</span>
</td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold ${ <span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold ${
user.loggedIn user.isSuspended
? 'bg-red-100 text-red-700'
: user.loggedIn
? 'bg-green-100 text-green-700' ? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-600' : 'bg-gray-100 text-gray-600'
}`}> }`}>
<div className={`w-1.5 h-1.5 rounded-full ${user.loggedIn ? 'bg-green-500' : 'bg-gray-400'}`} /> <div className={`w-1.5 h-1.5 rounded-full ${user.isSuspended ? 'bg-red-500' : user.loggedIn ? 'bg-green-500' : 'bg-gray-400'}`} />
{user.loggedIn ? 'Active Now' : 'Offline'} {user.isSuspended ? 'Suspended' : user.loggedIn ? 'Active Now' : 'Offline'}
</span> </span>
</td> </td>
<td className="px-6 py-4 text-right relative"> <td className="px-6 py-4 text-right relative">
@@ -308,6 +331,10 @@ const Customers: React.FC = () => {
<button onClick={() => handleEditClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-[#475569] font-medium border-b border-[#f1f5f9]"> <button onClick={() => handleEditClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-[#475569] font-medium border-b border-[#f1f5f9]">
<Edit2 size={16} /> Edit User <Edit2 size={16} /> Edit User
</button> </button>
<button onClick={() => handleSuspendToggle(user)} className={`w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 font-medium border-b border-[#f1f5f9] ${user.isSuspended ? 'text-amber-600' : 'text-red-500'}`}>
{user.isSuspended ? <UserCheck size={16} /> : <UserMinus size={16} />}
{user.isSuspended ? 'Unsuspend User' : 'Suspend User'}
</button>
<button onClick={() => handleDeleteClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-red-600 font-medium font-bold"> <button onClick={() => handleDeleteClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-red-600 font-medium font-bold">
<Trash2 size={16} /> Delete User <Trash2 size={16} /> Delete User
</button> </button>
@@ -339,9 +366,13 @@ const Customers: React.FC = () => {
</button> </button>
{openMenuId === user.id && ( {openMenuId === user.id && (
<div className="absolute right-0 mt-2 w-40 bg-white rounded-xl shadow-2xl border border-[#e2e8f0] z-50 overflow-hidden"> <div className="absolute right-0 mt-2 w-40 bg-white rounded-xl shadow-2xl border border-[#e2e8f0] z-50 overflow-hidden">
<button onClick={() => handleEditClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-[#475569] font-medium"> <button onClick={() => handleEditClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-[#475569] font-medium border-b border-[#f1f5f9]">
<Edit2 size={16} /> Edit <Edit2 size={16} /> Edit
</button> </button>
<button onClick={() => handleSuspendToggle(user)} className={`w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 font-medium border-b border-[#f1f5f9] ${user.isSuspended ? 'text-amber-600' : 'text-red-500'}`}>
{user.isSuspended ? <UserCheck size={16} /> : <UserMinus size={16} />}
{user.isSuspended ? 'Unsuspend' : 'Suspend'}
</button>
<button onClick={() => handleDeleteClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-red-600 font-medium"> <button onClick={() => handleDeleteClick(user)} className="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-50 text-red-600 font-medium">
<Trash2 size={16} /> Delete <Trash2 size={16} /> Delete
</button> </button>
@@ -363,6 +394,11 @@ const Customers: React.FC = () => {
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-lg border border-blue-100 uppercase tracking-wider"> <span className="px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-lg border border-blue-100 uppercase tracking-wider">
RIT STUDENT RIT STUDENT
</span> </span>
{user.isSuspended && (
<span className="px-2 py-0.5 bg-red-50 text-red-600 text-[10px] font-bold rounded-lg border border-red-100 uppercase tracking-wider">
SUSPENDED
</span>
)}
<span className="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[10px] font-bold rounded-lg border border-indigo-100 uppercase tracking-wider"> <span className="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[10px] font-bold rounded-lg border border-indigo-100 uppercase tracking-wider">
{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS {user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS
</span> </span>

View File

@@ -5,7 +5,8 @@ import {
Wallet, Wallet,
ArrowRight, ArrowRight,
RotateCcw, RotateCcw,
Star Star,
UserMinus
} from 'lucide-react'; } from 'lucide-react';
import { import {
AreaChart, AreaChart,
@@ -29,7 +30,8 @@ const StoreDashboard = () => {
totalSales: 0, totalSales: 0,
activeOrders: 0, activeOrders: 0,
dailyCustomers: 0, dailyCustomers: 0,
revenueGrowth: 0 revenueGrowth: 0,
suspendedUserCount: 0
}); });
const [trendingItems, setTrendingItems] = useState<any[]>([]); const [trendingItems, setTrendingItems] = useState<any[]>([]);
const [salesData, setSalesData] = useState<any[]>([]); const [salesData, setSalesData] = useState<any[]>([]);
@@ -107,7 +109,8 @@ const StoreDashboard = () => {
totalSales: data.stats.totalSales, totalSales: data.stats.totalSales,
activeOrders: data.stats.activeOrders, activeOrders: data.stats.activeOrders,
dailyCustomers: data.stats.dailyCustomers, dailyCustomers: data.stats.dailyCustomers,
revenueGrowth: data.stats.growth revenueGrowth: data.stats.growth,
suspendedUserCount: data.stats.suspendedUserCount
}); });
} }
@@ -290,8 +293,21 @@ const StoreDashboard = () => {
</div> </div>
</div> </div>
{/* Restricted Accounts Card */}
<div title="Monitor suspended customer accounts" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[130px] flex flex-col justify-between group cursor-pointer hover:border-red-500/30 transition-all">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-50 text-red-500 rounded-xl group-hover:scale-110 transition-transform">
<UserMinus size={20} />
</div>
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Restricted Accounts</h4>
</div>
<div className="text-center">
<h2 className="text-4xl font-black text-slate-800 tracking-tighter">{stats?.suspendedUserCount || 0}</h2>
</div>
</div>
{/* Expenses Card */} {/* Expenses Card */}
<div title="Monitor store operational expenditures" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[150px] flex flex-col justify-between group cursor-pointer hover:border-[#0f4475]/30 transition-all"> <div title="Monitor store operational expenditures" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[130px] flex flex-col justify-between group cursor-pointer hover:border-[#0f4475]/30 transition-all">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-amber-100 text-[#0f4475] rounded-xl shadow-sm"> <div className="p-2 bg-amber-100 text-[#0f4475] rounded-xl shadow-sm">
<Wallet size={20} /> <Wallet size={20} />

View File

@@ -117,12 +117,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (response.ok) { if (response.ok) {
const updatedUserDto = await response.json(); const updatedUserDto = await response.json();
// The DTO from backend might have different structure, let's map it back to User type
const updatedUser: User = { const updatedUser: User = {
id: updatedUserDto.id, id: updatedUserDto.id,
name: updatedUserDto.name, name: updatedUserDto.name,
mobileNumber: updatedUserDto.mobileNumber, mobileNumber: updatedUserDto.mobileNumber,
isLoggedIn: updatedUserDto.loggedIn, isLoggedIn: updatedUserDto.loggedIn,
isSuspended: updatedUserDto.suspended,
ritzTokenBalance: updatedUserDto.ritzTokenBalance ritzTokenBalance: updatedUserDto.ritzTokenBalance
}; };
setUser(updatedUser); setUser(updatedUser);
@@ -137,6 +137,33 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} }
}; };
// Background suspension check
useEffect(() => {
if (!user) return;
const checkSuspensionStatus = async () => {
try {
const response = await fetch(`${API_BASE_URL}/user/${user.mobileNumber}`, { cache: 'no-store' });
if (response.status === 404) {
logout(); // User deleted
return;
}
if (response.ok) {
const data = await response.json();
if (data.isSuspended || data.suspended) {
console.log("Account suspended. Logging out...");
logout();
}
}
} catch (error) {
console.error('Error checking user status:', error);
}
};
const intervalId = setInterval(checkSuspensionStatus, 30000); // Check every 30 seconds
return () => clearInterval(intervalId);
}, [user]);
return ( return (
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin, updateProfile }}> <AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin, updateProfile }}>
{children} {children}

View File

@@ -152,6 +152,124 @@
color: var(--primary); color: var(--primary);
} }
.error-message {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
color: #ef4444;
padding: 12px 16px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
/* Suspension Modal Styles */
.suspension-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.suspension-modal {
background: rgba(255, 255, 255, 0.95);
width: 100%;
max-width: 400px;
border-radius: 28px;
padding: 40px 32px;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.suspension-icon {
width: 80px;
height: 80px;
background: #fef2f2;
color: #ef4444;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
}
.suspension-modal h2 {
font-size: 24px;
font-weight: 800;
color: #1e293b;
margin-bottom: 12px;
}
.suspension-modal p {
color: #64748b;
font-size: 15px;
line-height: 1.6;
margin-bottom: 24px;
}
.suspension-details {
background: #f8fafc;
padding: 16px;
border-radius: 16px;
margin-bottom: 32px;
border: 1px solid #e2e8f0;
}
.suspension-details span {
color: #475569;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
display: block;
}
.suspension-close-btn {
width: 100%;
padding: 16px;
background: #231651;
color: white;
border: none;
border-radius: 16px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.suspension-close-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(35, 22, 81, 0.4);
}
.suspension-close-btn:active {
transform: translateY(0);
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-zoomIn {
animation: zoomIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.login-button { .login-button {
background-color: var(--primary); background-color: var(--primary);
color: white; color: white;

View File

@@ -15,6 +15,7 @@ const LoginScreen: React.FC = () => {
const [pin, setPin] = useState(''); const [pin, setPin] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isSuspended, setIsSuspended] = useState(false);
const handleMobileSubmit = async (e: React.FormEvent) => { const handleMobileSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -53,6 +54,8 @@ const LoginScreen: React.FC = () => {
if (result.success) { if (result.success) {
navigate('/'); navigate('/');
} else if (result.isSuspended) {
setIsSuspended(true);
} else { } else {
setError(result.message); setError(result.message);
} }
@@ -215,6 +218,24 @@ const LoginScreen: React.FC = () => {
<p>RIT Canteen Management System</p> <p>RIT Canteen Management System</p>
</div> </div>
</div> </div>
{isSuspended && (
<div className="suspension-overlay">
<div className="suspension-modal animate-zoomIn">
<div className="suspension-icon">
<Lock size={40} />
</div>
<h2>Account Suspended</h2>
<p>Your access to the RIT Canteen system has been restricted by an administrator.</p>
<div className="suspension-details">
<span>Please contact the Admin or Manager to resolve this issue.</span>
</div>
<button className="suspension-close-btn" onClick={() => setIsSuspended(false)}>
Back to Login
</button>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -33,6 +33,7 @@ export interface User {
mobileNumber: string; mobileNumber: string;
name: string; name: string;
isLoggedIn: boolean; isLoggedIn: boolean;
isSuspended: boolean;
ritzTokenBalance: number; ritzTokenBalance: number;
} }