Multiple user support added

This commit is contained in:
Sidharth Prabhu
2026-04-21 12:59:07 +05:30
parent c4ca865181
commit 0c65542fb9
9 changed files with 518 additions and 67 deletions

View File

@@ -62,4 +62,31 @@ public class SystemAuthController {
userService.deleteManager(id); // Using existing delete logic
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()));
}
}
}

View File

@@ -28,29 +28,10 @@ public class SystemUserService {
@PostConstruct
public void init() {
// Handle migration from old credentials if they exist
repository.findByEmail("admin@ritcanteen.com").ifPresent(oldUser -> {
boolean hasNewAdmin = repository.findByEmail("admin").isPresent();
if (hasNewAdmin) {
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 {
// Ensure at least one 'MASTER' user exists in the database
List<SystemUser> masters = repository.findByRole("MASTER");
if (masters.isEmpty()) {
SystemUser master = new SystemUser();
master.setName("Admin Master");
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.setViewOnly(false);
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);
}
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) {
if (id != null) {
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) {
System.out.println(">>> Attempting authentication for: " + email);
// 1. Try Database First
Optional<SystemUser> user = repository.findByEmail(email);
if (user.isPresent()) {
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) {
return user;
}
} 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();
}
}

View File

@@ -27,6 +27,7 @@ import Ritz from './pages/Ritz.tsx';
import RitzCirculation from './pages/RitzCirculation.tsx';
import ManageWallets from './pages/ManageWallets.tsx';
import ManageCoupons from './pages/ManageCoupons.tsx';
import Settings from './pages/Settings.tsx';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
@@ -106,6 +107,9 @@ function App() {
<Route path="stores/managers" element={<Managers />} />
<Route path="stores/staffs" element={<Staff />} />
<Route path="stores/stalls" element={<Stalls />} />
{/* Settings */}
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</Router>

View File

@@ -1,4 +1,5 @@
import { Bell, Search, Settings, HelpCircle, Calendar } from 'lucide-react';
import { Link } from 'react-router-dom';
const Header = () => {
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>
</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} />
</button>
</Link>
<button className="p-2 text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100 rounded-lg transition-all">
<HelpCircle size={20} />

View File

@@ -164,6 +164,7 @@ const Sidebar = () => {
sessionStorage.removeItem('isLoggedIn');
sessionStorage.removeItem('userRole');
sessionStorage.removeItem('userPermissions');
localStorage.removeItem('systemUser');
navigate('/login');
};

View File

@@ -151,7 +151,12 @@ const Dashboard = () => {
</button>
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Dashboard</h2>
</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">
<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.
@@ -175,14 +180,6 @@ const Dashboard = () => {
View Full Report
</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>

View File

@@ -27,6 +27,7 @@ const Login = () => {
sessionStorage.removeItem('isLoggedIn');
sessionStorage.removeItem('userRole');
sessionStorage.removeItem('userPermissions');
localStorage.removeItem('systemUser');
}, []);
const handleLogin = async (e: React.FormEvent) => {
@@ -51,6 +52,10 @@ const Login = () => {
sessionStorage.setItem('isLoggedIn', 'true');
sessionStorage.setItem('userRole', user.role.toLowerCase());
sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || []));
// Persist user profile for personalized greetings and settings
localStorage.setItem('systemUser', JSON.stringify(user));
navigate('/store-dashboard');
} else {
const error = await response.text();

View 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;

View File

@@ -167,39 +167,16 @@ const StoreDashboard = () => {
</button>
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Store Dashboard</h2>
</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 className="flex items-center gap-3">
<div className="flex items-center gap-2">
<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>
{/* Action buttons or profile placeholder can go here if needed later */}
</div>
</div>