diff --git a/src/food-market.web/src/App.tsx b/src/food-market.web/src/App.tsx index 63cf853..d4cc66b 100644 --- a/src/food-market.web/src/App.tsx +++ b/src/food-market.web/src/App.tsx @@ -13,6 +13,8 @@ import { ProductsPage } from '@/pages/ProductsPage' import { ProductEditPage } from '@/pages/ProductEditPage' import { MoySkladImportPage } from '@/pages/MoySkladImportPage' import { OrganizationSettingsPage } from '@/pages/OrganizationSettingsPage' +import { EmployeesPage } from '@/pages/EmployeesPage' +import { EmployeeRolesPage } from '@/pages/EmployeeRolesPage' import { StockPage } from '@/pages/StockPage' import { StockMovementsPage } from '@/pages/StockMovementsPage' import { SuppliesPage } from '@/pages/SuppliesPage' @@ -60,6 +62,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> diff --git a/src/food-market.web/src/components/AppLayout.tsx b/src/food-market.web/src/components/AppLayout.tsx index 490c683..682a5ff 100644 --- a/src/food-market.web/src/components/AppLayout.tsx +++ b/src/food-market.web/src/components/AppLayout.tsx @@ -6,7 +6,7 @@ import { logout } from '@/lib/auth' import { cn } from '@/lib/utils' import { LayoutDashboard, Package, FolderTree, Ruler, Tag, - Users, Warehouse, Store as StoreIcon, Globe, LogOut, Download, + Users, Warehouse, Store as StoreIcon, Globe, LogOut, Download, UserCog, Shield, Boxes, History, TruckIcon, ShoppingCart, Settings, Menu, X, } from 'lucide-react' import { Logo } from './Logo' @@ -57,6 +57,8 @@ function buildNav(): NavSection[] { { to: '/settings/organization', icon: Settings, label: 'Общие' }, { to: '/catalog/stores', icon: Warehouse, label: 'Склады' }, { to: '/catalog/retail-points', icon: StoreIcon, label: 'Кассы' }, + { to: '/settings/employees', icon: UserCog, label: 'Сотрудники' }, + { to: '/settings/employee-roles', icon: Shield, label: 'Роли' }, ]}, ] } diff --git a/src/food-market.web/src/pages/EmployeeRolesPage.tsx b/src/food-market.web/src/pages/EmployeeRolesPage.tsx new file mode 100644 index 0000000..f00dc9d --- /dev/null +++ b/src/food-market.web/src/pages/EmployeeRolesPage.tsx @@ -0,0 +1,198 @@ +import { useState } from 'react' +import { Plus, Trash2 } from 'lucide-react' +import { ListPageShell } from '@/components/ListPageShell' +import { DataTable } from '@/components/DataTable' +import { Pagination } from '@/components/Pagination' +import { SearchBar } from '@/components/SearchBar' +import { Button } from '@/components/Button' +import { Modal } from '@/components/Modal' +import { Field, TextInput, TextArea, Checkbox } from '@/components/Field' +import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' + +const URL = '/api/organization/employee-roles' + +export interface RolePermissions { + productsView: boolean; productsEdit: boolean; productsDelete: boolean + productGroupsManage: boolean; priceTypesManage: boolean + suppliesView: boolean; suppliesEdit: boolean; suppliesPost: boolean; suppliesDelete: boolean + retailSalesOperate: boolean; retailSalesRefund: boolean + counterpartiesView: boolean; counterpartiesEdit: boolean + reportsView: boolean; stocksView: boolean + orgSettingsManage: boolean; employeesManage: boolean; rolesManage: boolean + storesManage: boolean; retailPointsManage: boolean +} + +export interface EmployeeRoleDto { + id: string; name: string; description: string | null + isSystem: boolean; sortOrder: number; permissions: RolePermissions +} + +interface Form { + id?: string + name: string + description: string + isSystem: boolean + permissions: RolePermissions +} + +const blankPerms = (): RolePermissions => ({ + productsView: false, productsEdit: false, productsDelete: false, + productGroupsManage: false, priceTypesManage: false, + suppliesView: false, suppliesEdit: false, suppliesPost: false, suppliesDelete: false, + retailSalesOperate: false, retailSalesRefund: false, + counterpartiesView: false, counterpartiesEdit: false, + reportsView: false, stocksView: false, + orgSettingsManage: false, employeesManage: false, rolesManage: false, + storesManage: false, retailPointsManage: false, +}) + +const blankForm = (): Form => ({ name: '', description: '', isSystem: false, permissions: blankPerms() }) + +const PERM_GROUPS: { title: string; perms: { key: keyof RolePermissions; label: string }[] }[] = [ + { title: 'Каталог', perms: [ + { key: 'productsView', label: 'Просмотр товаров' }, + { key: 'productsEdit', label: 'Редактирование товаров' }, + { key: 'productsDelete', label: 'Удаление товаров' }, + { key: 'productGroupsManage', label: 'Управление группами товаров' }, + { key: 'priceTypesManage', label: 'Управление типами цен' }, + ]}, + { title: 'Закупки', perms: [ + { key: 'suppliesView', label: 'Просмотр приёмок' }, + { key: 'suppliesEdit', label: 'Редактирование приёмок' }, + { key: 'suppliesPost', label: 'Проведение приёмок' }, + { key: 'suppliesDelete', label: 'Удаление приёмок' }, + ]}, + { title: 'Продажи', perms: [ + { key: 'retailSalesOperate', label: 'Работа на кассе' }, + { key: 'retailSalesRefund', label: 'Возвраты' }, + ]}, + { title: 'Контрагенты', perms: [ + { key: 'counterpartiesView', label: 'Просмотр' }, + { key: 'counterpartiesEdit', label: 'Редактирование' }, + ]}, + { title: 'Отчёты и остатки', perms: [ + { key: 'reportsView', label: 'Просмотр отчётов' }, + { key: 'stocksView', label: 'Просмотр остатков' }, + ]}, + { title: 'Настройки организации', perms: [ + { key: 'orgSettingsManage', label: 'Общие настройки' }, + { key: 'employeesManage', label: 'Сотрудники' }, + { key: 'rolesManage', label: 'Роли' }, + { key: 'storesManage', label: 'Склады' }, + { key: 'retailPointsManage', label: 'Кассы' }, + ]}, +] + +export function EmployeeRolesPage() { + const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList(URL) + const { create, update, remove } = useCatalogMutations(URL, URL) + const [form, setForm] = useState
(null) + + const save = async () => { + if (!form) return + const payload = { name: form.name, description: form.description || null, permissions: form.permissions } + if (form.id) await update.mutateAsync({ id: form.id, input: payload }) + else await create.mutateAsync(payload) + setForm(null) + } + + return ( + <> + + + + + } + footer={data && data.total > 0 && ( + + )} + > + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => setForm({ + id: r.id, name: r.name, description: r.description ?? '', + isSystem: r.isSystem, permissions: { ...blankPerms(), ...r.permissions }, + })} + columns={[ + { header: 'Название', cell: (r) => ( +
+
{r.name}
+ {r.description &&
{r.description}
} +
+ )}, + { header: 'Тип', width: '140px', cell: (r) => r.isSystem + ? Системная + : Кастомная }, + { header: 'Прав активно', width: '160px', className: 'text-right', cell: (r) => { + const count = Object.values(r.permissions ?? {}).filter(Boolean).length + return {count} / {Object.keys(r.permissions ?? {}).length} + }}, + ]} + /> +
+ + setForm(null)} + title={form?.id ? 'Редактировать роль' : 'Новая роль'} + width="max-w-2xl" + footer={ + <> + {form?.id && !form.isSystem && ( + + )} + + + + } + > + {form && ( +
+ + setForm({ ...form, name: e.target.value })} /> + + +