User suspension added
This commit is contained in:
@@ -14,7 +14,7 @@ public class WebConfig {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedOriginPatterns("http://*", "https://*", "file://*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
|
||||
@@ -138,4 +138,18 @@ public class UserController {
|
||||
userService.deleteUser(id);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ public class DashboardStats {
|
||||
private double growth;
|
||||
private long totalExpenses;
|
||||
private long periodExpenses;
|
||||
private long suspendedUserCount;
|
||||
|
||||
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.periodRevenue = periodRevenue;
|
||||
this.activeOrders = activeOrders;
|
||||
@@ -19,6 +20,7 @@ public class DashboardStats {
|
||||
this.growth = growth;
|
||||
this.totalExpenses = totalExpenses;
|
||||
this.periodExpenses = periodExpenses;
|
||||
this.suspendedUserCount = suspendedUserCount;
|
||||
}
|
||||
|
||||
public long getTotalSales() { return totalSales; }
|
||||
@@ -41,4 +43,7 @@ public class DashboardStats {
|
||||
|
||||
public long getPeriodExpenses() { return periodExpenses; }
|
||||
public void setPeriodExpenses(long periodExpenses) { this.periodExpenses = periodExpenses; }
|
||||
|
||||
public long getSuspendedUserCount() { return suspendedUserCount; }
|
||||
public void setSuspendedUserCount(long suspendedUserCount) { this.suspendedUserCount = suspendedUserCount; }
|
||||
}
|
||||
|
||||
@@ -43,15 +43,17 @@ public class LoginResponse {
|
||||
private String mobileNumber;
|
||||
private String name;
|
||||
private boolean isLoggedIn;
|
||||
private boolean isSuspended;
|
||||
private java.math.BigDecimal ritzTokenBalance;
|
||||
|
||||
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.mobileNumber = mobileNumber;
|
||||
this.name = name;
|
||||
this.isLoggedIn = isLoggedIn;
|
||||
this.isSuspended = isSuspended;
|
||||
this.ritzTokenBalance = ritzTokenBalance;
|
||||
}
|
||||
|
||||
@@ -67,6 +69,9 @@ public class LoginResponse {
|
||||
public boolean isLoggedIn() { return isLoggedIn; }
|
||||
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 void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; }
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ public class User {
|
||||
@Column(nullable = false)
|
||||
private java.math.BigDecimal ritzTokenBalance = java.math.BigDecimal.ZERO;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||
private boolean isSuspended = false;
|
||||
|
||||
@Column(nullable = true)
|
||||
private LocalDateTime lastLoginAt;
|
||||
|
||||
@@ -54,6 +57,9 @@ public class User {
|
||||
public java.math.BigDecimal getRitzTokenBalance() { return 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 void setPinHash(String pinHash) { this.pinHash = pinHash; }
|
||||
|
||||
|
||||
@@ -19,4 +19,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
@org.springframework.data.jpa.repository.Lock(jakarta.persistence.LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("SELECT u FROM User u WHERE u.id = :id")
|
||||
Optional<User> findByIdWithLock(@org.springframework.data.repository.query.Param("id") Long id);
|
||||
|
||||
long countByIsSuspended(boolean isSuspended);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class UserService {
|
||||
userRepository.save(user);
|
||||
|
||||
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);
|
||||
@@ -79,6 +79,10 @@ public class UserService {
|
||||
|
||||
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())) {
|
||||
return new LoginResponse(false, "Incorrect PIN. Please try again.");
|
||||
}
|
||||
@@ -88,7 +92,7 @@ public class UserService {
|
||||
userRepository.save(user);
|
||||
|
||||
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);
|
||||
@@ -154,7 +158,7 @@ public class UserService {
|
||||
return null;
|
||||
}
|
||||
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.getName(),
|
||||
user.isLoggedIn(),
|
||||
user.isSuspended(),
|
||||
user.getRitzTokenBalance()
|
||||
));
|
||||
}
|
||||
@@ -212,7 +217,28 @@ public class UserService {
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ interface UserDto {
|
||||
mobileNumber: string;
|
||||
name: string;
|
||||
loggedIn: boolean;
|
||||
isSuspended: boolean;
|
||||
ritzTokenBalance: number;
|
||||
}
|
||||
|
||||
@@ -66,7 +67,13 @@ const Customers: React.FC = () => {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
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);
|
||||
} else {
|
||||
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 newErrors: {[key: string]: string} = {};
|
||||
if (!editForm.name.trim()) newErrors.name = 'Full Name is required';
|
||||
@@ -283,19 +309,16 @@ const Customers: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
</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">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold ${
|
||||
user.loggedIn
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
user.isSuspended
|
||||
? 'bg-red-100 text-red-700'
|
||||
: user.loggedIn
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${user.loggedIn ? 'bg-green-500' : 'bg-gray-400'}`} />
|
||||
{user.loggedIn ? 'Active Now' : 'Offline'}
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${user.isSuspended ? 'bg-red-500' : user.loggedIn ? 'bg-green-500' : 'bg-gray-400'}`} />
|
||||
{user.isSuspended ? 'Suspended' : user.loggedIn ? 'Active Now' : 'Offline'}
|
||||
</span>
|
||||
</td>
|
||||
<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]">
|
||||
<Edit2 size={16} /> Edit User
|
||||
</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">
|
||||
<Trash2 size={16} /> Delete User
|
||||
</button>
|
||||
@@ -339,9 +366,13 @@ const Customers: React.FC = () => {
|
||||
</button>
|
||||
{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">
|
||||
<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
|
||||
</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">
|
||||
<Trash2 size={16} /> Delete
|
||||
</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">
|
||||
RIT STUDENT
|
||||
</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">
|
||||
₹{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS
|
||||
</span>
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
Wallet,
|
||||
ArrowRight,
|
||||
RotateCcw,
|
||||
Star
|
||||
Star,
|
||||
UserMinus
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
AreaChart,
|
||||
@@ -29,7 +30,8 @@ const StoreDashboard = () => {
|
||||
totalSales: 0,
|
||||
activeOrders: 0,
|
||||
dailyCustomers: 0,
|
||||
revenueGrowth: 0
|
||||
revenueGrowth: 0,
|
||||
suspendedUserCount: 0
|
||||
});
|
||||
const [trendingItems, setTrendingItems] = useState<any[]>([]);
|
||||
const [salesData, setSalesData] = useState<any[]>([]);
|
||||
@@ -107,7 +109,8 @@ const StoreDashboard = () => {
|
||||
totalSales: data.stats.totalSales,
|
||||
activeOrders: data.stats.activeOrders,
|
||||
dailyCustomers: data.stats.dailyCustomers,
|
||||
revenueGrowth: data.stats.growth
|
||||
revenueGrowth: data.stats.growth,
|
||||
suspendedUserCount: data.stats.suspendedUserCount
|
||||
});
|
||||
}
|
||||
|
||||
@@ -290,8 +293,21 @@ const StoreDashboard = () => {
|
||||
</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 */}
|
||||
<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="p-2 bg-amber-100 text-[#0f4475] rounded-xl shadow-sm">
|
||||
<Wallet size={20} />
|
||||
|
||||
@@ -117,12 +117,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (response.ok) {
|
||||
const updatedUserDto = await response.json();
|
||||
// The DTO from backend might have different structure, let's map it back to User type
|
||||
const updatedUser: User = {
|
||||
id: updatedUserDto.id,
|
||||
name: updatedUserDto.name,
|
||||
mobileNumber: updatedUserDto.mobileNumber,
|
||||
isLoggedIn: updatedUserDto.loggedIn,
|
||||
isSuspended: updatedUserDto.suspended,
|
||||
ritzTokenBalance: updatedUserDto.ritzTokenBalance
|
||||
};
|
||||
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 (
|
||||
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin, updateProfile }}>
|
||||
{children}
|
||||
|
||||
@@ -152,6 +152,124 @@
|
||||
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 {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
@@ -15,6 +15,7 @@ const LoginScreen: React.FC = () => {
|
||||
const [pin, setPin] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSuspended, setIsSuspended] = useState(false);
|
||||
|
||||
const handleMobileSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -53,6 +54,8 @@ const LoginScreen: React.FC = () => {
|
||||
|
||||
if (result.success) {
|
||||
navigate('/');
|
||||
} else if (result.isSuspended) {
|
||||
setIsSuspended(true);
|
||||
} else {
|
||||
setError(result.message);
|
||||
}
|
||||
@@ -215,6 +218,24 @@ const LoginScreen: React.FC = () => {
|
||||
<p>RIT Canteen Management System</p>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface User {
|
||||
mobileNumber: string;
|
||||
name: string;
|
||||
isLoggedIn: boolean;
|
||||
isSuspended: boolean;
|
||||
ritzTokenBalance: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user