Multiple user support added
This commit is contained in:
@@ -62,4 +62,31 @@ public class SystemAuthController {
|
|||||||
userService.deleteManager(id); // Using existing delete logic
|
userService.deleteManager(id); // Using existing delete logic
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admins")
|
||||||
|
public ResponseEntity<List<SystemUser>> getAdmins() {
|
||||||
|
return ResponseEntity.ok(userService.getMasters());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/admins")
|
||||||
|
public ResponseEntity<SystemUser> addAdmin(@RequestBody SystemUser admin) {
|
||||||
|
return ResponseEntity.ok(userService.createMaster(admin));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/update-master")
|
||||||
|
public ResponseEntity<?> updateMaster(@RequestBody Map<String, Object> data) {
|
||||||
|
try {
|
||||||
|
Object idObj = data.get("id");
|
||||||
|
Long id = (idObj != null) ? Long.valueOf(idObj.toString()) : 0L;
|
||||||
|
|
||||||
|
String email = (String) data.get("email");
|
||||||
|
String password = (String) data.get("password");
|
||||||
|
String name = (String) data.get("name");
|
||||||
|
|
||||||
|
userService.updateMasterAccount(id, email, password, name);
|
||||||
|
return ResponseEntity.ok(Map.of("success", true, "message", "Credentials updated successfully"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(Map.of("success", false, "message", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,29 +28,10 @@ public class SystemUserService {
|
|||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
// Handle migration from old credentials if they exist
|
// Ensure at least one 'MASTER' user exists in the database
|
||||||
repository.findByEmail("admin@ritcanteen.com").ifPresent(oldUser -> {
|
List<SystemUser> masters = repository.findByRole("MASTER");
|
||||||
boolean hasNewAdmin = repository.findByEmail("admin").isPresent();
|
|
||||||
if (hasNewAdmin) {
|
if (masters.isEmpty()) {
|
||||||
repository.delete(oldUser);
|
|
||||||
System.out.println(">>> REMOVED OBSOLETE MASTER USER: admin@ritcanteen.com");
|
|
||||||
} else {
|
|
||||||
oldUser.setEmail(masterUsername);
|
|
||||||
oldUser.setPassword(passwordEncoder.encode(masterPassword));
|
|
||||||
repository.save(oldUser);
|
|
||||||
System.out.println(">>> MIGRATED MASTER USER: admin / admin");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure 'admin' user exists and has the correct password
|
|
||||||
Optional<SystemUser> adminUser = repository.findByEmail(masterUsername);
|
|
||||||
if (adminUser.isPresent()) {
|
|
||||||
SystemUser admin = adminUser.get();
|
|
||||||
admin.setPassword(passwordEncoder.encode(masterPassword));
|
|
||||||
admin.setRole("MASTER");
|
|
||||||
repository.save(admin);
|
|
||||||
System.out.println(">>> UPDATED MASTER USER PASSWORD: " + masterUsername + " / " + masterPassword);
|
|
||||||
} else {
|
|
||||||
SystemUser master = new SystemUser();
|
SystemUser master = new SystemUser();
|
||||||
master.setName("Admin Master");
|
master.setName("Admin Master");
|
||||||
master.setEmail(masterUsername);
|
master.setEmail(masterUsername);
|
||||||
@@ -59,7 +40,9 @@ public class SystemUserService {
|
|||||||
master.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"));
|
master.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"));
|
||||||
master.setViewOnly(false);
|
master.setViewOnly(false);
|
||||||
repository.save(master);
|
repository.save(master);
|
||||||
System.out.println(">>> SEEDED MASTER USER: " + masterUsername + " / " + masterPassword);
|
System.out.println(">>> SEEDED DEFAULT MASTER USER (Failsafe Source): " + masterUsername + " / " + masterPassword);
|
||||||
|
} else {
|
||||||
|
System.out.println(">>> MASTER USER(S) FOUND IN DATABASE. Skipping default seeding.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,24 +66,81 @@ public class SystemUserService {
|
|||||||
return repository.save(staff);
|
return repository.save(staff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<SystemUser> getMasters() {
|
||||||
|
return repository.findByRole("MASTER");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SystemUser createMaster(SystemUser admin) {
|
||||||
|
admin.setRole("MASTER");
|
||||||
|
admin.setPassword(passwordEncoder.encode(admin.getPassword()));
|
||||||
|
// Grant full permissions by default for new admins
|
||||||
|
admin.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"));
|
||||||
|
return repository.save(admin);
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteManager(Long id) {
|
public void deleteManager(Long id) {
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
repository.deleteById(id);
|
repository.deleteById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateMasterAccount(Long id, String email, String password, String name) {
|
||||||
|
SystemUser user;
|
||||||
|
if (id == null || id == 0) {
|
||||||
|
// If failsafe user (ID 0) or null, try to find the first master in the database
|
||||||
|
user = repository.findByRole("MASTER").stream().findFirst()
|
||||||
|
.orElseGet(() -> {
|
||||||
|
SystemUser newMaster = new SystemUser();
|
||||||
|
newMaster.setRole("MASTER");
|
||||||
|
newMaster.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"));
|
||||||
|
return newMaster;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
user = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Master account not found with ID: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email != null) user.setEmail(email);
|
||||||
|
if (password != null && !password.isEmpty()) {
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
}
|
||||||
|
if (name != null) user.setName(name);
|
||||||
|
repository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<SystemUser> authenticate(String email, String password) {
|
public Optional<SystemUser> authenticate(String email, String password) {
|
||||||
System.out.println(">>> Attempting authentication for: " + email);
|
System.out.println(">>> Attempting authentication for: " + email);
|
||||||
|
|
||||||
|
// 1. Try Database First
|
||||||
Optional<SystemUser> user = repository.findByEmail(email);
|
Optional<SystemUser> user = repository.findByEmail(email);
|
||||||
if (user.isPresent()) {
|
if (user.isPresent()) {
|
||||||
boolean matches = passwordEncoder.matches(password, user.get().getPassword());
|
boolean matches = passwordEncoder.matches(password, user.get().getPassword());
|
||||||
System.out.println(">>> User found. Password match: " + matches);
|
System.out.println(">>> User found in DB. Password match: " + matches);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
System.out.println(">>> User NOT found: " + email);
|
System.out.println(">>> User NOT found in DB. Checking Failsafe eligibility...");
|
||||||
|
|
||||||
|
// 2. Try Failsafe (Properties) - ONLY if no Master users exist in DB
|
||||||
|
List<SystemUser> masters = repository.findByRole("MASTER");
|
||||||
|
if (masters.isEmpty()) {
|
||||||
|
if (email.equals(masterUsername) && password.equals(masterPassword)) {
|
||||||
|
System.out.println(">>> FAILSAFE AUTHENTICATION SUCCESSFUL (No DB Master Found)");
|
||||||
|
|
||||||
|
SystemUser failsafeUser = new SystemUser();
|
||||||
|
failsafeUser.setId(0L);
|
||||||
|
failsafeUser.setName("Failsafe Admin");
|
||||||
|
failsafeUser.setEmail(masterUsername);
|
||||||
|
failsafeUser.setRole("MASTER");
|
||||||
|
failsafeUser.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"));
|
||||||
|
return Optional.of(failsafeUser);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.out.println(">>> Failsafe disabled because custom master account exists in database.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import Ritz from './pages/Ritz.tsx';
|
|||||||
import RitzCirculation from './pages/RitzCirculation.tsx';
|
import RitzCirculation from './pages/RitzCirculation.tsx';
|
||||||
import ManageWallets from './pages/ManageWallets.tsx';
|
import ManageWallets from './pages/ManageWallets.tsx';
|
||||||
import ManageCoupons from './pages/ManageCoupons.tsx';
|
import ManageCoupons from './pages/ManageCoupons.tsx';
|
||||||
|
import Settings from './pages/Settings.tsx';
|
||||||
|
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -106,6 +107,9 @@ function App() {
|
|||||||
<Route path="stores/managers" element={<Managers />} />
|
<Route path="stores/managers" element={<Managers />} />
|
||||||
<Route path="stores/staffs" element={<Staff />} />
|
<Route path="stores/staffs" element={<Staff />} />
|
||||||
<Route path="stores/stalls" element={<Stalls />} />
|
<Route path="stores/stalls" element={<Stalls />} />
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
|
<Route path="settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Bell, Search, Settings, HelpCircle, Calendar } from 'lucide-react';
|
import { Bell, Search, Settings, HelpCircle, Calendar } from 'lucide-react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const currentDate = new Date().toLocaleDateString('en-US', {
|
const currentDate = new Date().toLocaleDateString('en-US', {
|
||||||
@@ -33,9 +34,9 @@ const Header = () => {
|
|||||||
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span>
|
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button className="p-2 text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100 rounded-lg transition-all">
|
<Link to="/settings" className="p-2 text-[#64748b] hover:text-[#231651] hover:bg-indigo-50 rounded-lg transition-all">
|
||||||
<Settings size={20} />
|
<Settings size={20} />
|
||||||
</button>
|
</Link>
|
||||||
|
|
||||||
<button className="p-2 text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100 rounded-lg transition-all">
|
<button className="p-2 text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100 rounded-lg transition-all">
|
||||||
<HelpCircle size={20} />
|
<HelpCircle size={20} />
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const Sidebar = () => {
|
|||||||
sessionStorage.removeItem('isLoggedIn');
|
sessionStorage.removeItem('isLoggedIn');
|
||||||
sessionStorage.removeItem('userRole');
|
sessionStorage.removeItem('userRole');
|
||||||
sessionStorage.removeItem('userPermissions');
|
sessionStorage.removeItem('userPermissions');
|
||||||
|
localStorage.removeItem('systemUser');
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,12 @@ const Dashboard = () => {
|
|||||||
</button>
|
</button>
|
||||||
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Dashboard</h2>
|
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Dashboard</h2>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-black text-slate-800">Good morning</h1>
|
<h1 className="text-2xl font-black text-slate-800">
|
||||||
|
Good morning, {(() => {
|
||||||
|
const saved = localStorage.getItem('systemUser');
|
||||||
|
return saved ? JSON.parse(saved).name : 'Partner';
|
||||||
|
})()}
|
||||||
|
</h1>
|
||||||
<p className="text-[10px] text-slate-400 font-medium flex items-center gap-1.5">
|
<p className="text-[10px] text-slate-400 font-medium flex items-center gap-1.5">
|
||||||
<span className="p-0.5 bg-slate-200 rounded text-slate-500">i</span>
|
<span className="p-0.5 bg-slate-200 rounded text-slate-500">i</span>
|
||||||
The default time settings for the merchant view are from 12:00 AM to 11:59 PM.
|
The default time settings for the merchant view are from 12:00 AM to 11:59 PM.
|
||||||
@@ -175,14 +180,6 @@ const Dashboard = () => {
|
|||||||
View Full Report
|
View Full Report
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="relative group">
|
|
||||||
<select title="Filter by store type or category" className="appearance-none bg-white border border-slate-200 rounded-xl px-4 py-2 pr-10 text-xs font-bold text-slate-600 outline-none focus:border-[#0f4475] transition-all cursor-pointer shadow-sm">
|
|
||||||
<option>Store Type: All</option>
|
|
||||||
</select>
|
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
||||||
<ChevronRight size={14} className="rotate-90 text-slate-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const Login = () => {
|
|||||||
sessionStorage.removeItem('isLoggedIn');
|
sessionStorage.removeItem('isLoggedIn');
|
||||||
sessionStorage.removeItem('userRole');
|
sessionStorage.removeItem('userRole');
|
||||||
sessionStorage.removeItem('userPermissions');
|
sessionStorage.removeItem('userPermissions');
|
||||||
|
localStorage.removeItem('systemUser');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
@@ -51,6 +52,10 @@ const Login = () => {
|
|||||||
sessionStorage.setItem('isLoggedIn', 'true');
|
sessionStorage.setItem('isLoggedIn', 'true');
|
||||||
sessionStorage.setItem('userRole', user.role.toLowerCase());
|
sessionStorage.setItem('userRole', user.role.toLowerCase());
|
||||||
sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || []));
|
sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || []));
|
||||||
|
|
||||||
|
// Persist user profile for personalized greetings and settings
|
||||||
|
localStorage.setItem('systemUser', JSON.stringify(user));
|
||||||
|
|
||||||
navigate('/store-dashboard');
|
navigate('/store-dashboard');
|
||||||
} else {
|
} else {
|
||||||
const error = await response.text();
|
const error = await response.text();
|
||||||
|
|||||||
399
frontend/src/pages/Settings.tsx
Normal file
399
frontend/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Mail,
|
||||||
|
Lock,
|
||||||
|
ShieldCheck,
|
||||||
|
Save,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
Shield,
|
||||||
|
Key,
|
||||||
|
Users,
|
||||||
|
UserPlus,
|
||||||
|
ArrowRight,
|
||||||
|
ShieldAlert
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<'profile' | 'team'>('profile');
|
||||||
|
const [currentUser, setCurrentUser] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('systemUser');
|
||||||
|
return saved ? JSON.parse(saved) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Profile Form State
|
||||||
|
const [profileData, setProfileData] = useState({
|
||||||
|
name: currentUser?.name || '',
|
||||||
|
email: currentUser?.email || '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Team State
|
||||||
|
const [admins, setAdmins] = useState<any[]>([]);
|
||||||
|
const [newAdmin, setNewAdmin] = useState({ name: '', email: '', password: '' });
|
||||||
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'team') {
|
||||||
|
fetchAdmins();
|
||||||
|
}
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
const fetchAdmins = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/system/admins');
|
||||||
|
const data = await response.json();
|
||||||
|
setAdmins(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch admins');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setProfileData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateProfile = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (profileData.password && profileData.password !== profileData.confirmPassword) {
|
||||||
|
setStatus('error');
|
||||||
|
setMessage('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('loading');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/system/update-master', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: currentUser?.id,
|
||||||
|
name: profileData.name,
|
||||||
|
email: profileData.email,
|
||||||
|
password: profileData.password || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setStatus('success');
|
||||||
|
setMessage(data.message);
|
||||||
|
const updatedUser = { ...currentUser, name: profileData.name, email: profileData.email };
|
||||||
|
localStorage.setItem('systemUser', JSON.stringify(updatedUser));
|
||||||
|
setCurrentUser(updatedUser);
|
||||||
|
setTimeout(() => setStatus('idle'), 3000);
|
||||||
|
} else {
|
||||||
|
setStatus('error');
|
||||||
|
setMessage(data.message || 'Update failed');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('error');
|
||||||
|
setMessage('Connection error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddAdmin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setStatus('loading');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/system/admins', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(newAdmin)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setStatus('success');
|
||||||
|
setMessage('New administrator added');
|
||||||
|
setNewAdmin({ name: '', email: '', password: '' });
|
||||||
|
setShowAddModal(false);
|
||||||
|
fetchAdmins();
|
||||||
|
setTimeout(() => setStatus('idle'), 3000);
|
||||||
|
} else {
|
||||||
|
setStatus('error');
|
||||||
|
setMessage('Failed to add administrator');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('error');
|
||||||
|
setMessage('Connection error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 max-w-6xl mx-auto">
|
||||||
|
<div className="mb-8 flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-black text-[#231651] mb-2">Settings & Security</h1>
|
||||||
|
<p className="text-[#64748b]">Manage your account and system access control</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex bg-gray-100 p-1.5 rounded-2xl w-fit">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('profile')}
|
||||||
|
className={`px-6 py-2.5 rounded-xl font-bold text-sm transition-all flex items-center gap-2 ${activeTab === 'profile' ? 'bg-white text-[#231651] shadow-sm' : 'text-[#64748b] hover:text-[#231651]'}`}
|
||||||
|
>
|
||||||
|
<User size={18} />
|
||||||
|
My Account
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('team')}
|
||||||
|
className={`px-6 py-2.5 rounded-xl font-bold text-sm transition-all flex items-center gap-2 ${activeTab === 'team' ? 'bg-white text-[#231651] shadow-sm' : 'text-[#64748b] hover:text-[#231651]'}`}
|
||||||
|
>
|
||||||
|
<Users size={18} />
|
||||||
|
Team Management
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{activeTab === 'profile' ? (
|
||||||
|
<motion.div
|
||||||
|
key="profile"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
className="grid grid-cols-1 lg:grid-cols-3 gap-8"
|
||||||
|
>
|
||||||
|
{/* Left Card: Summary */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="bg-white rounded-3xl p-6 border border-[#e2e8f0] shadow-sm text-center">
|
||||||
|
<div className="w-24 h-24 bg-gradient-to-br from-[#231651] to-[#6366f1] rounded-full mx-auto mb-4 flex items-center justify-center text-white shadow-xl shadow-indigo-100">
|
||||||
|
<User size={40} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-[#1e293b]">{currentUser?.name}</h3>
|
||||||
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 bg-indigo-50 text-indigo-600 rounded-full text-xs font-bold mt-2 uppercase tracking-wider">
|
||||||
|
<Shield size={12} />
|
||||||
|
{currentUser?.role}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 pt-8 border-t border-gray-100 text-left">
|
||||||
|
<div className="flex items-center gap-3 text-sm text-[#64748b] mb-4">
|
||||||
|
<Mail size={16} className="text-indigo-500" />
|
||||||
|
<span className="truncate">{currentUser?.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-sm text-[#64748b]">
|
||||||
|
<ShieldCheck size={16} className="text-green-500" />
|
||||||
|
<span>Full System Access</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Card: Form */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white rounded-3xl p-8 border border-[#e2e8f0] shadow-sm">
|
||||||
|
<form onSubmit={handleUpdateProfile} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-[#64748b] mb-2 uppercase tracking-wide">Display Name</label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={profileData.name}
|
||||||
|
onChange={handleProfileChange}
|
||||||
|
className="w-full bg-gray-50 border border-transparent focus:border-indigo-500 focus:bg-white rounded-xl py-3 pl-12 pr-4 outline-none transition-all font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-[#64748b] mb-2 uppercase tracking-wide">Email Address</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={profileData.email}
|
||||||
|
onChange={handleProfileChange}
|
||||||
|
className="w-full bg-gray-50 border border-transparent focus:border-indigo-500 focus:bg-white rounded-xl py-3 pl-12 pr-4 outline-none transition-all font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-px bg-gray-100 my-4" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-[#64748b] mb-2 uppercase tracking-wide">New Password</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={profileData.password}
|
||||||
|
onChange={handleProfileChange}
|
||||||
|
className="w-full bg-gray-50 border border-transparent focus:border-indigo-500 focus:bg-white rounded-xl py-3 pl-12 pr-4 outline-none transition-all font-medium"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-[#64748b] mb-2 uppercase tracking-wide">Confirm New Password</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={profileData.confirmPassword}
|
||||||
|
onChange={handleProfileChange}
|
||||||
|
className="w-full bg-gray-50 border border-transparent focus:border-indigo-500 focus:bg-white rounded-xl py-3 pl-12 pr-4 outline-none transition-all font-medium"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-6 border-t border-gray-50">
|
||||||
|
<div className="flex-1">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{status === 'success' && (
|
||||||
|
<motion.div initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0 }} className="flex items-center gap-2 text-green-600 font-bold text-sm">
|
||||||
|
<CheckCircle2 size={16} /> {message}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
{status === 'error' && (
|
||||||
|
<motion.div initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0 }} className="flex items-center gap-2 text-red-600 font-bold text-sm">
|
||||||
|
<AlertCircle size={16} /> {message}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={status === 'loading'} className="bg-[#231651] text-white px-8 py-3 rounded-xl font-bold flex items-center gap-2 hover:bg-opacity-90 transition-all disabled:opacity-50">
|
||||||
|
<Save size={18} /> Save Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="team"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-[#231651]">Administrator Team</h3>
|
||||||
|
<p className="text-sm text-[#64748b]">Found {admins.length} active system administrators</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddModal(true)}
|
||||||
|
className="bg-gradient-to-r from-[#231651] to-[#6366f1] text-white px-6 py-3 rounded-xl font-bold flex items-center gap-2 shadow-lg shadow-indigo-100 hover:scale-[1.02] transition-all"
|
||||||
|
>
|
||||||
|
<UserPlus size={18} />
|
||||||
|
Add New Admin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{admins.map((admin) => (
|
||||||
|
<div key={admin.id} className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm hover:shadow-md transition-all group">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="w-12 h-12 bg-gray-100 rounded-2xl flex items-center justify-center text-[#231651] group-hover:bg-[#231651] group-hover:text-white transition-colors">
|
||||||
|
<User size={24} />
|
||||||
|
</div>
|
||||||
|
{admin.id === currentUser?.id && (
|
||||||
|
<span className="text-[10px] bg-green-100 text-green-700 font-black px-2 py-1 rounded-md uppercase">You</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h4 className="font-bold text-[#1e293b] truncate border-b border-gray-50 pb-2 mb-3">{admin.name}</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs text-[#64748b]">
|
||||||
|
<Mail size={14} className="text-indigo-400" />
|
||||||
|
<span className="truncate">{admin.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-[#64748b]">
|
||||||
|
<ShieldCheck size={14} className="text-green-500" />
|
||||||
|
<span>{admin.role} ACCESS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Add Admin Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showAddModal && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setShowAddModal(false)} className="absolute inset-0 bg-[#231651]/20 backdrop-blur-sm" />
|
||||||
|
<motion.div initial={{ scale: 0.9, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.9, opacity: 0 }} className="bg-white w-full max-w-md rounded-[32px] overflow-hidden shadow-2xl relative z-10">
|
||||||
|
<div className="bg-[#231651] p-6 text-white text-center">
|
||||||
|
<div className="w-16 h-16 bg-white/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UserPlus size={32} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold">New Administrator</h3>
|
||||||
|
<p className="text-sm text-indigo-200">Grant full system access to a team member</p>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleAddAdmin} className="p-8 space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black text-[#64748b] mb-2 uppercase tracking-widest">Full Name</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
value={newAdmin.name}
|
||||||
|
onChange={(e) => setNewAdmin({...newAdmin, name: e.target.value})}
|
||||||
|
className="w-full bg-gray-50 border border-gray-100 rounded-xl py-3 px-4 outline-none focus:border-indigo-500 transition-all font-medium"
|
||||||
|
placeholder="e.g. John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black text-[#64748b] mb-2 uppercase tracking-widest">Login Email</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
value={newAdmin.email}
|
||||||
|
onChange={(e) => setNewAdmin({...newAdmin, email: e.target.value})}
|
||||||
|
className="w-full bg-gray-50 border border-gray-100 rounded-xl py-3 px-4 outline-none focus:border-indigo-500 transition-all font-medium"
|
||||||
|
placeholder="john@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black text-[#64748b] mb-2 uppercase tracking-widest">Initial Password</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
value={newAdmin.password}
|
||||||
|
onChange={(e) => setNewAdmin({...newAdmin, password: e.target.value})}
|
||||||
|
className="w-full bg-gray-50 border border-gray-100 rounded-xl py-3 px-4 outline-none focus:border-indigo-500 transition-all font-medium"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 border border-amber-100 p-4 rounded-2xl flex gap-3 text-amber-700">
|
||||||
|
<ShieldAlert size={20} className="shrink-0" />
|
||||||
|
<p className="text-[10px] leading-relaxed font-bold">
|
||||||
|
CRITICAL: This user will have full MASTER privileges. They can manage sales, tokens, and other administrators.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<button type="button" onClick={() => setShowAddModal(false)} className="flex-1 py-3 rounded-xl font-bold text-gray-500 hover:bg-gray-50 transition-all">Cancel</button>
|
||||||
|
<button type="submit" disabled={status === 'loading'} className="flex-1 bg-[#231651] text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-indigo-700 transition-all disabled:opacity-50">
|
||||||
|
{status === 'loading' ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <>Create User <ArrowRight size={16} /></>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
@@ -167,39 +167,16 @@ const StoreDashboard = () => {
|
|||||||
</button>
|
</button>
|
||||||
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Store Dashboard</h2>
|
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Store Dashboard</h2>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-black text-slate-800">Good morning, RIT Canteen</h1>
|
<h1 className="text-2xl font-black text-slate-800">
|
||||||
|
Good morning, {(() => {
|
||||||
|
const saved = localStorage.getItem('systemUser');
|
||||||
|
return saved ? JSON.parse(saved).name : 'Partner';
|
||||||
|
})()}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
{/* Action buttons or profile placeholder can go here if needed later */}
|
||||||
<div className="relative group">
|
|
||||||
<select title="Switch established store location" className="appearance-none bg-white border border-slate-200 rounded-xl px-4 py-2 pr-10 text-xs font-bold text-slate-600 outline-none focus:border-[#0f4475] transition-all cursor-pointer shadow-sm">
|
|
||||||
<option>RIT Canteen</option>
|
|
||||||
</select>
|
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
||||||
<ChevronRight size={14} className="rotate-90 text-slate-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative group">
|
|
||||||
<select title="Toggle between available display views" className="appearance-none bg-white border border-slate-200 rounded-xl px-4 py-2 pr-10 text-xs font-bold text-slate-600 outline-none focus:border-[#0f4475] transition-all cursor-pointer shadow-sm">
|
|
||||||
<option>All</option>
|
|
||||||
</select>
|
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
||||||
<ChevronRight size={14} className="rotate-90 text-slate-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button title="Synchronize latest live data from backend" className="p-2 bg-white border border-slate-200 rounded-xl text-slate-400 hover:text-slate-600 transition-all shadow-sm">
|
|
||||||
<RotateCcw size={18} />
|
|
||||||
</button>
|
|
||||||
<button title="Mark this store dashboard as favorite for quick access" className="p-2 bg-white border border-slate-200 rounded-xl text-amber-500 hover:scale-110 transition-all shadow-sm">
|
|
||||||
<Star size={18} fill="#f59e0b" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div title="Authenticated User: Abiram" className="w-8 h-8 rounded-full bg-[#0f4475] text-white flex items-center justify-center font-bold text-sm shadow-md cursor-pointer hover:ring-2 ring-[#0f4475]/20 transition-all">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user