diff --git a/backend/migrations/082_users_custom_roles.sql b/backend/migrations/082_users_custom_roles.sql new file mode 100644 index 0000000..1b83206 --- /dev/null +++ b/backend/migrations/082_users_custom_roles.sql @@ -0,0 +1,2 @@ +-- Remove role CHECK constraint so custom role keys (e.g. custom_123) can be stored +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_check; diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts index 9f042fe..e336dbf 100644 --- a/backend/src/routes/users.ts +++ b/backend/src/routes/users.ts @@ -59,12 +59,13 @@ const users: FastifyPluginAsync = async (fastify) => { const { email, password, name, role = 'housekeeper', phone, position } = request.body // Role creation permissions + const isCustomRole = (r: string) => r.startsWith('custom_') const hotelAdminAllowedRoles = ['manager', 'housekeeper', 'receptionist', 'accountant', 'security', 'technician'] const managerAllowedRoles = ['housekeeper', 'receptionist', 'accountant', 'security', 'technician'] - if (request.user.role === 'hotel_admin' && !hotelAdminAllowedRoles.includes(role)) { + if (request.user.role === 'hotel_admin' && !hotelAdminAllowedRoles.includes(role) && !isCustomRole(role)) { return reply.code(403).send({ error: 'Недостаточно прав для создания этой роли' }) } - if (request.user.role === 'manager' && !managerAllowedRoles.includes(role)) { + if (request.user.role === 'manager' && !managerAllowedRoles.includes(role) && !isCustomRole(role)) { return reply.code(403).send({ error: 'Недостаточно прав для создания этой роли' }) } @@ -131,10 +132,11 @@ const users: FastifyPluginAsync = async (fastify) => { return reply.code(403).send({ error: 'Нельзя назначить роль системного администратора' }) } // Managers can change role but not to manager/super_admin + const isCustomRole = (r: string) => r.startsWith('custom_') const managerAllowedRoles = ['housekeeper', 'receptionist', 'accountant', 'security', 'technician'] if (request.user.role === 'super_admin' || - (request.user.role === 'hotel_admin' && managerAllowedRoles.concat(['manager']).includes(request.body.role)) || - (request.user.role === 'manager' && managerAllowedRoles.includes(request.body.role))) { + (request.user.role === 'hotel_admin' && (managerAllowedRoles.concat(['manager']).includes(request.body.role) || isCustomRole(request.body.role))) || + (request.user.role === 'manager' && (managerAllowedRoles.includes(request.body.role) || isCustomRole(request.body.role)))) { updates.push(`role = $${idx}`); values.push(request.body.role); idx++ } } diff --git a/src/pages/UsersPage.tsx b/src/pages/UsersPage.tsx index f2bb86c..ee6836d 100644 --- a/src/pages/UsersPage.tsx +++ b/src/pages/UsersPage.tsx @@ -242,12 +242,13 @@ const INITIAL_ROLE_PERMISSIONS: RolePermissions[] = [ // ── Helpers ──────────────────────────────────────────────────────────────────── function mapRole(r: string): StaffRole { - if (r === 'housekeeper') return 'housekeeper' - if (r === 'receptionist') return 'receptionist' - if (r === 'accountant') return 'accountant' - if (r === 'security') return 'security' - if (r === 'technician') return 'technician' - if (r === 'hotel_admin') return 'hotel_admin' + if (r === 'housekeeper') return 'housekeeper' + if (r === 'receptionist') return 'receptionist' + if (r === 'accountant') return 'accountant' + if (r === 'security') return 'security' + if (r === 'technician') return 'technician' + if (r === 'hotel_admin') return 'hotel_admin' + if (r.startsWith('custom_')) return r as StaffRole return 'hotel_manager' } @@ -281,6 +282,8 @@ function UserModal({ onClose: () => void onSave: (u: StaffUser, password?: string) => void }) { + const { savedPermissions } = useRolePermissions() + const customRoles = savedPermissions.filter(r => !r.isSystem) const [form, setForm] = useState({ firstName: user?.firstName ?? '', lastName: user?.lastName ?? '', @@ -320,7 +323,7 @@ function UserModal({ }, password || undefined) } - const suggestions = DEFAULT_POSITIONS[form.role] + const suggestions = DEFAULT_POSITIONS[form.role as keyof typeof DEFAULT_POSITIONS] ?? [] return ( Сис. администратор — роль нельзя изменить ) : ( -
- {(Object.entries(ROLE_META) as [StaffRole, typeof ROLE_META[StaffRole]][]) - .filter(([id]) => id !== 'hotel_admin') - .map(([id, meta]) => ( - - ))} +
+
+ {(Object.entries(ROLE_META) as [StaffRole, typeof ROLE_META[StaffRole]][]) + .filter(([id]) => id !== 'hotel_admin') + .map(([id, meta]) => ( + + ))} +
+ {customRoles.length > 0 && ( +
+ {customRoles.map(r => ( + + ))} +
+ )}
)}
@@ -598,7 +623,7 @@ function RolesTab() { } // ── Create / Delete ──────────────────────────────────────────────────────── - const createRole = () => { + const createRole = async () => { const name = newRoleName.trim() if (!name) return const colors = ['#EC4899', '#14B8A6', '#F59E0B', '#6366F1', '#84CC16'] @@ -614,6 +639,21 @@ function RolesTab() { setSelectedRoleId(newRole.id) setNewRoleName('') setAddingRole(false) + // Persist immediately so the role survives a page refresh + if (slug) { + try { + await api.rolePermissions.save(slug, key, { + name: newRole.name, + color: newRole.color, + isSystem: false, + permissions: newRole.permissions, + homePage: null, + }) + await reloadContext() + } catch (err) { + console.error('Failed to save new role', err) + } + } } const deleteRole = async (id: string) => { @@ -907,6 +947,7 @@ type PageTab = typeof PAGE_TABS[number]['id'] export function UsersPage() { const { user: currentUser } = useAuth() + const { savedPermissions } = useRolePermissions() const slug = currentUser?.hotelSlug ?? '' const [tab, setTab] = useState('staff') const [users, setUsers] = useState([]) @@ -937,7 +978,7 @@ export function UsersPage() { const handleSave = async (u: StaffUser, password?: string) => { const fullName = `${u.firstName} ${u.lastName}`.trim() - const backendRoleMap: Record = { + const backendRoleMap: Record = { hotel_admin: 'hotel_admin', hotel_manager: 'manager', receptionist: 'receptionist', @@ -946,7 +987,7 @@ export function UsersPage() { security: 'security', technician: 'technician', } - const backendRole = backendRoleMap[u.role] ?? 'manager' + const backendRole = backendRoleMap[u.role] ?? u.role try { if (!u.id) { const created = await api.users.create(slug, { @@ -1107,8 +1148,10 @@ export function UsersPage() { - - {meta.label} + + {meta ? meta.label : (savedPermissions.find(r => r.roleKey === u.role)?.name ?? u.role)}

{u.position}