Skip to content

Commit f48769e

Browse files
committed
feat(settings): add settings UI with users and roles pages
- Add /settings route with sidebar navigation - Add ProtectedRoute component for capability-based access control - Add SettingsLayout component with Users and Roles navigation - Add RolesPage for read-only role viewing with capability grouping - Update Header with Settings link in user dropdown (gated by capability) - Wrap App with PermissionsProvider - Add URY Role and URY User Role to fixtures export
1 parent 42f402e commit f48769e

7 files changed

Lines changed: 346 additions & 14 deletions

File tree

pos/src/App.tsx

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import POSOpeningProvider from './components/POSOpeningProvider';
99
import ScreenSizeProvider from './components/ScreenSizeProvider';
1010
import { ToastProvider } from './components/ui/toast';
1111
import { usePOSStore } from './store/pos-store';
12-
import { useEffect } from 'react';
12+
import { useEffect, lazy, Suspense } from 'react';
13+
import { PermissionsProvider } from './contexts/PermissionsContext';
14+
import { ProtectedRoute } from './components/ProtectedRoute';
15+
16+
// Lazy load settings pages
17+
const SettingsLayout = lazy(() => import('./components/SettingsLayout'));
18+
const UsersPage = lazy(() => import('./pages/admin/UsersPage'));
19+
const RolesPage = lazy(() => import('./pages/admin/RolesPage'));
1320

1421
function App() {
1522
const {
@@ -19,25 +26,45 @@ function App() {
1926
useEffect(() => {
2027
initializeApp();
2128
}, [initializeApp]);
29+
2230
return (
2331
<>
2432
<ToastProvider />
2533
<ScreenSizeProvider>
2634
<AuthGuard>
2735
<POSOpeningProvider>
28-
<Router basename="/pos">
29-
<div className="flex flex-col h-screen bg-gray-100 font-inter">
30-
<Header />
31-
<div className="flex-1 overflow-hidden">
32-
<Routes>
33-
<Route path="/" element={<POS/>} />
34-
<Route path="/orders" element={<Orders />} />
35-
<Route path="/table" element={<Table />} />
36-
</Routes>
36+
<PermissionsProvider>
37+
<Router basename="/pos">
38+
<div className="flex flex-col h-screen bg-gray-100 font-inter">
39+
<Header />
40+
<div className="flex-1 overflow-hidden">
41+
<Suspense fallback={<div className="flex items-center justify-center h-full">Loading...</div>}>
42+
<Routes>
43+
<Route path="/" element={<POS/>} />
44+
<Route path="/orders" element={<Orders />} />
45+
<Route path="/table" element={<Table />} />
46+
47+
{/* Settings Routes */}
48+
<Route path="/settings" element={
49+
<ProtectedRoute requiredCapability="users.manage">
50+
<SettingsLayout />
51+
</ProtectedRoute>
52+
}>
53+
<Route index element={<UsersPage />} />
54+
<Route path="users" element={<UsersPage />} />
55+
<Route path="roles" element={
56+
<ProtectedRoute requiredCapability="roles.manage">
57+
<RolesPage />
58+
</ProtectedRoute>
59+
} />
60+
</Route>
61+
</Routes>
62+
</Suspense>
63+
</div>
64+
<Footer />
3765
</div>
38-
<Footer />
39-
</div>
40-
</Router>
66+
</Router>
67+
</PermissionsProvider>
4168
</POSOpeningProvider>
4269
</AuthGuard>
4370
</ScreenSizeProvider>

pos/src/components/Header.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
11
import { useState, useEffect, useRef } from 'react';
2-
import { Link, useLocation } from 'react-router-dom';
2+
import { Link, useLocation, useNavigate } from 'react-router-dom';
33
import {
44
Command,
55
User,
66
ChevronDown,
77
Monitor,
88
LogOut,
99
RefreshCw,
10+
Settings,
1011
} from 'lucide-react';
1112
import { Button, Input } from './ui';
1213
import { useRootStore } from '../store/root-store';
1314
import { usePOSStore } from '../store/pos-store';
1415
import type { RootState } from '../store/root-store';
1516
import { logout } from '../lib/auth-api';
1617
import { showToast } from './ui/toast';
18+
import { usePermissions } from '../contexts/PermissionsContext';
1719

1820
const Header = () => {
1921
const [showUserMenu, setShowUserMenu] = useState(false);
2022
const userMenuRef = useRef<HTMLDivElement>(null);
2123
const user = useRootStore((state: RootState) => state.user);
2224
const searchInputRef = useRef<HTMLInputElement>(null);
2325
const location = useLocation();
26+
const navigate = useNavigate();
2427
const { searchQuery, setSearchQuery } = usePOSStore();
2528
const { orderSearchQuery, setOrderSearchQuery } = useRootStore();
2629
const [orderSearchInput, setOrderSearchInput] = useState(orderSearchQuery);
30+
const { hasCapability } = usePermissions();
31+
32+
// Check if user has any settings-related capability
33+
const canAccessSettings = hasCapability('users.manage') || hasCapability('roles.manage');
2734

2835
// Determine placeholder and handlers based on route
2936
let searchPlaceholder = 'Search orders, menu items, or customers...';
@@ -155,6 +162,19 @@ const Header = () => {
155162
<p className="text-sm text-gray-500">{user?.name || ''}</p>
156163
</div>
157164
<div className="py-2">
165+
{canAccessSettings && (
166+
<Button
167+
variant="ghost"
168+
className="flex justify-start items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
169+
onClick={() => {
170+
setShowUserMenu(false);
171+
navigate('/settings');
172+
}}
173+
>
174+
<Settings className="w-4 h-4 mr-3" />
175+
Settings
176+
</Button>
177+
)}
158178
<Button
159179
variant="ghost"
160180
className="flex justify-start items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Navigate } from 'react-router-dom';
2+
import { usePermissions } from '../contexts/PermissionsContext';
3+
4+
interface ProtectedRouteProps {
5+
children: React.ReactNode;
6+
requiredCapability: string;
7+
}
8+
9+
export function ProtectedRoute({ children, requiredCapability }: ProtectedRouteProps) {
10+
const { hasCapability, isLoading } = usePermissions();
11+
12+
if (isLoading) {
13+
return <div className="flex items-center justify-center h-full">Loading...</div>;
14+
}
15+
16+
if (!hasCapability(requiredCapability)) {
17+
return <Navigate to="/" replace />;
18+
}
19+
20+
return <>{children}</>;
21+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
2+
import { Users, Shield, ArrowLeft, Settings } from 'lucide-react';
3+
import { Button } from './ui';
4+
import { usePermissions } from '../contexts/PermissionsContext';
5+
6+
const SettingsLayout = () => {
7+
const location = useLocation();
8+
const navigate = useNavigate();
9+
const { hasCapability } = usePermissions();
10+
11+
const navItems = [
12+
{
13+
path: '/settings/users',
14+
label: 'Users',
15+
icon: Users,
16+
capability: 'users.manage',
17+
},
18+
{
19+
path: '/settings/roles',
20+
label: 'Roles',
21+
icon: Shield,
22+
capability: 'roles.manage',
23+
},
24+
].filter(item => hasCapability(item.capability));
25+
26+
const currentPath = location.pathname;
27+
28+
return (
29+
<div className="h-full flex flex-col bg-gray-50">
30+
{/* Settings Header */}
31+
<div className="bg-white border-b border-gray-200 px-6 py-4">
32+
<div className="flex items-center justify-between">
33+
<div className="flex items-center space-x-3">
34+
<Settings className="w-6 h-6 text-gray-700" />
35+
<h1 className="text-xl font-semibold text-gray-900">Settings</h1>
36+
</div>
37+
<Button
38+
variant="ghost"
39+
onClick={() => navigate('/')}
40+
className="flex items-center space-x-2 text-gray-600 hover:text-gray-900"
41+
>
42+
<ArrowLeft className="w-4 h-4" />
43+
<span>Back to POS</span>
44+
</Button>
45+
</div>
46+
</div>
47+
48+
{/* Settings Content */}
49+
<div className="flex-1 flex overflow-hidden">
50+
{/* Sidebar */}
51+
<aside className="w-64 bg-white border-r border-gray-200">
52+
<nav className="p-4 space-y-1">
53+
{navItems.map((item) => {
54+
const isActive = currentPath === item.path || currentPath.startsWith(item.path);
55+
const Icon = item.icon;
56+
57+
return (
58+
<Link
59+
key={item.path}
60+
to={item.path}
61+
className={`flex items-center space-x-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${
62+
isActive
63+
? 'bg-primary-50 text-primary-700'
64+
: 'text-gray-700 hover:bg-gray-100'
65+
}`}
66+
>
67+
<Icon className={`w-5 h-5 ${isActive ? 'text-primary-600' : 'text-gray-500'}`} />
68+
<span>{item.label}</span>
69+
</Link>
70+
);
71+
})}
72+
</nav>
73+
</aside>
74+
75+
{/* Main Content Area */}
76+
<main className="flex-1 overflow-auto p-6">
77+
<Outlet />
78+
</main>
79+
</div>
80+
</div>
81+
);
82+
};
83+
84+
export default SettingsLayout;

0 commit comments

Comments
 (0)