From 080564f2b284d9b568d24b0146c1e846f5471f6e Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:44:53 +0500 Subject: [PATCH] feat(roles): permissions matrix grouped by section + clone-from-template flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RolePermissions расширен с 21 до 32 флагов: добавлены UnitsManage, DemandsView/Edit/Post (отгрузка контрагенту, не путать с RetailSales), CounterpartiesDelete, InventoryEdit, LossEdit, EnterEdit (склад-операции), ReportsFinanceView, ReportsStockView (тонкие отчётные права), CashRegistersManage и IntegrationsManage (отдельно от OrgSettingsManage). UI EmployeeRolesPage: - 7 групп вместо 6: Каталог / Закупки / Продажи / Контрагенты / Склад-Остатки / Отчёты / Настройки. Все секции аккордеоном внутри модалки (как было — flex-col), но с правильным грануляр-списком. - Системные роли — чекбоксы disabled (только просмотр; имя/описание редактируются). - При [+ Добавить роль] — сначала открывается модалка выбора шаблона: Пустой / Копия Администратора / Копия любой существующей. Дальше открывается основная модалка с предзаполненной матрицей. allPerms() помощник на фронте — зеркало RolePermissions.All() с бэка, для шаблона «Копия Администратора» в clone-flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Organizations/RolePermissions.cs | 29 ++++- .../Migrations/AppDbContextModelSnapshot.cs | 14 ++- .../src/pages/EmployeeRolesPage.tsx | 113 +++++++++++++++--- 3 files changed, 135 insertions(+), 21 deletions(-) diff --git a/src/food-market.domain/Organizations/RolePermissions.cs b/src/food-market.domain/Organizations/RolePermissions.cs index 616f48e..3a9b11f 100644 --- a/src/food-market.domain/Organizations/RolePermissions.cs +++ b/src/food-market.domain/Organizations/RolePermissions.cs @@ -10,6 +10,7 @@ public class RolePermissions public bool ProductsDelete { get; set; } public bool ProductGroupsManage { get; set; } public bool PriceTypesManage { get; set; } + public bool UnitsManage { get; set; } // Закупки public bool SuppliesView { get; set; } @@ -17,17 +18,28 @@ public class RolePermissions public bool SuppliesPost { get; set; } public bool SuppliesDelete { get; set; } - // Продажи (POS) + // Продажи (отгрузка контрагенту + POS) + public bool DemandsView { get; set; } + public bool DemandsEdit { get; set; } + public bool DemandsPost { get; set; } public bool RetailSalesOperate { get; set; } public bool RetailSalesRefund { get; set; } // Контрагенты public bool CounterpartiesView { get; set; } public bool CounterpartiesEdit { get; set; } + public bool CounterpartiesDelete { get; set; } - // Отчёты и остатки - public bool ReportsView { get; set; } + // Склад / Остатки public bool StocksView { get; set; } + public bool InventoryEdit { get; set; } + public bool LossEdit { get; set; } + public bool EnterEdit { get; set; } + + // Отчёты + public bool ReportsView { get; set; } + public bool ReportsFinanceView { get; set; } + public bool ReportsStockView { get; set; } // Настройки организации public bool OrgSettingsManage { get; set; } @@ -35,17 +47,22 @@ public class RolePermissions public bool RolesManage { get; set; } public bool StoresManage { get; set; } public bool RetailPointsManage { get; set; } + public bool CashRegistersManage { get; set; } + public bool IntegrationsManage { get; set; } /// Полный набор всех true — для системной роли «Администратор». public static RolePermissions All() => new() { ProductsView = true, ProductsEdit = true, ProductsDelete = true, - ProductGroupsManage = true, PriceTypesManage = true, + ProductGroupsManage = true, PriceTypesManage = true, UnitsManage = true, SuppliesView = true, SuppliesEdit = true, SuppliesPost = true, SuppliesDelete = true, + DemandsView = true, DemandsEdit = true, DemandsPost = true, RetailSalesOperate = true, RetailSalesRefund = true, - CounterpartiesView = true, CounterpartiesEdit = true, - ReportsView = true, StocksView = true, + CounterpartiesView = true, CounterpartiesEdit = true, CounterpartiesDelete = true, + StocksView = true, InventoryEdit = true, LossEdit = true, EnterEdit = true, + ReportsView = true, ReportsFinanceView = true, ReportsStockView = true, OrgSettingsManage = true, EmployeesManage = true, RolesManage = true, StoresManage = true, RetailPointsManage = true, + CashRegistersManage = true, IntegrationsManage = true, }; } diff --git a/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index 9beaa7d..c0f17b7 100644 --- a/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -1914,21 +1914,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) o.Property("ProductsDelete").HasColumnType("boolean"); o.Property("ProductGroupsManage").HasColumnType("boolean"); o.Property("PriceTypesManage").HasColumnType("boolean"); + o.Property("UnitsManage").HasColumnType("boolean"); o.Property("SuppliesView").HasColumnType("boolean"); o.Property("SuppliesEdit").HasColumnType("boolean"); o.Property("SuppliesPost").HasColumnType("boolean"); o.Property("SuppliesDelete").HasColumnType("boolean"); + o.Property("DemandsView").HasColumnType("boolean"); + o.Property("DemandsEdit").HasColumnType("boolean"); + o.Property("DemandsPost").HasColumnType("boolean"); o.Property("RetailSalesOperate").HasColumnType("boolean"); o.Property("RetailSalesRefund").HasColumnType("boolean"); o.Property("CounterpartiesView").HasColumnType("boolean"); o.Property("CounterpartiesEdit").HasColumnType("boolean"); - o.Property("ReportsView").HasColumnType("boolean"); + o.Property("CounterpartiesDelete").HasColumnType("boolean"); o.Property("StocksView").HasColumnType("boolean"); + o.Property("InventoryEdit").HasColumnType("boolean"); + o.Property("LossEdit").HasColumnType("boolean"); + o.Property("EnterEdit").HasColumnType("boolean"); + o.Property("ReportsView").HasColumnType("boolean"); + o.Property("ReportsFinanceView").HasColumnType("boolean"); + o.Property("ReportsStockView").HasColumnType("boolean"); o.Property("OrgSettingsManage").HasColumnType("boolean"); o.Property("EmployeesManage").HasColumnType("boolean"); o.Property("RolesManage").HasColumnType("boolean"); o.Property("StoresManage").HasColumnType("boolean"); o.Property("RetailPointsManage").HasColumnType("boolean"); + o.Property("CashRegistersManage").HasColumnType("boolean"); + o.Property("IntegrationsManage").HasColumnType("boolean"); o.HasKey("EmployeeRoleId"); o.ToJson("permissions"); o.WithOwner().HasForeignKey("EmployeeRoleId"); diff --git a/src/food-market.web/src/pages/EmployeeRolesPage.tsx b/src/food-market.web/src/pages/EmployeeRolesPage.tsx index f00dc9d..6a5f8a1 100644 --- a/src/food-market.web/src/pages/EmployeeRolesPage.tsx +++ b/src/food-market.web/src/pages/EmployeeRolesPage.tsx @@ -13,13 +13,16 @@ const URL = '/api/organization/employee-roles' export interface RolePermissions { productsView: boolean; productsEdit: boolean; productsDelete: boolean - productGroupsManage: boolean; priceTypesManage: boolean + productGroupsManage: boolean; priceTypesManage: boolean; unitsManage: boolean suppliesView: boolean; suppliesEdit: boolean; suppliesPost: boolean; suppliesDelete: boolean + demandsView: boolean; demandsEdit: boolean; demandsPost: boolean retailSalesOperate: boolean; retailSalesRefund: boolean - counterpartiesView: boolean; counterpartiesEdit: boolean - reportsView: boolean; stocksView: boolean + counterpartiesView: boolean; counterpartiesEdit: boolean; counterpartiesDelete: boolean + stocksView: boolean; inventoryEdit: boolean; lossEdit: boolean; enterEdit: boolean + reportsView: boolean; reportsFinanceView: boolean; reportsStockView: boolean orgSettingsManage: boolean; employeesManage: boolean; rolesManage: boolean - storesManage: boolean; retailPointsManage: boolean + storesManage: boolean; retailPointsManage: boolean; cashRegistersManage: boolean + integrationsManage: boolean } export interface EmployeeRoleDto { @@ -37,24 +40,42 @@ interface Form { const blankPerms = (): RolePermissions => ({ productsView: false, productsEdit: false, productsDelete: false, - productGroupsManage: false, priceTypesManage: false, + productGroupsManage: false, priceTypesManage: false, unitsManage: false, suppliesView: false, suppliesEdit: false, suppliesPost: false, suppliesDelete: false, + demandsView: false, demandsEdit: false, demandsPost: false, retailSalesOperate: false, retailSalesRefund: false, - counterpartiesView: false, counterpartiesEdit: false, - reportsView: false, stocksView: false, + counterpartiesView: false, counterpartiesEdit: false, counterpartiesDelete: false, + stocksView: false, inventoryEdit: false, lossEdit: false, enterEdit: false, + reportsView: false, reportsFinanceView: false, reportsStockView: false, orgSettingsManage: false, employeesManage: false, rolesManage: false, - storesManage: false, retailPointsManage: false, + storesManage: false, retailPointsManage: false, cashRegistersManage: false, + integrationsManage: false, +}) + +export const allPerms = (): RolePermissions => ({ + productsView: true, productsEdit: true, productsDelete: true, + productGroupsManage: true, priceTypesManage: true, unitsManage: true, + suppliesView: true, suppliesEdit: true, suppliesPost: true, suppliesDelete: true, + demandsView: true, demandsEdit: true, demandsPost: true, + retailSalesOperate: true, retailSalesRefund: true, + counterpartiesView: true, counterpartiesEdit: true, counterpartiesDelete: true, + stocksView: true, inventoryEdit: true, lossEdit: true, enterEdit: true, + reportsView: true, reportsFinanceView: true, reportsStockView: true, + orgSettingsManage: true, employeesManage: true, rolesManage: true, + storesManage: true, retailPointsManage: true, cashRegistersManage: true, + integrationsManage: true, }) const blankForm = (): Form => ({ name: '', description: '', isSystem: false, permissions: blankPerms() }) -const PERM_GROUPS: { title: string; perms: { key: keyof RolePermissions; label: string }[] }[] = [ +export 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: 'productGroupsManage', label: 'Управление группами' }, { key: 'priceTypesManage', label: 'Управление типами цен' }, + { key: 'unitsManage', label: 'Единицы измерения' }, ]}, { title: 'Закупки', perms: [ { key: 'suppliesView', label: 'Просмотр приёмок' }, @@ -63,16 +84,27 @@ const PERM_GROUPS: { title: string; perms: { key: keyof RolePermissions; label: { key: 'suppliesDelete', label: 'Удаление приёмок' }, ]}, { title: 'Продажи', perms: [ + { key: 'demandsView', label: 'Просмотр отгрузок' }, + { key: 'demandsEdit', label: 'Редактирование отгрузок' }, + { key: 'demandsPost', label: 'Проведение отгрузок' }, { key: 'retailSalesOperate', label: 'Работа на кассе' }, - { key: 'retailSalesRefund', label: 'Возвраты' }, + { key: 'retailSalesRefund', label: 'Возвраты на кассе' }, ]}, { title: 'Контрагенты', perms: [ { key: 'counterpartiesView', label: 'Просмотр' }, { key: 'counterpartiesEdit', label: 'Редактирование' }, + { key: 'counterpartiesDelete', label: 'Удаление' }, ]}, - { title: 'Отчёты и остатки', perms: [ - { key: 'reportsView', label: 'Просмотр отчётов' }, + { title: 'Склад / Остатки', perms: [ { key: 'stocksView', label: 'Просмотр остатков' }, + { key: 'inventoryEdit', label: 'Инвентаризация' }, + { key: 'lossEdit', label: 'Списание' }, + { key: 'enterEdit', label: 'Оприходование' }, + ]}, + { title: 'Отчёты', perms: [ + { key: 'reportsView', label: 'Сводные отчёты' }, + { key: 'reportsFinanceView', label: 'Финансы / касса' }, + { key: 'reportsStockView', label: 'Остатки и обороты' }, ]}, { title: 'Настройки организации', perms: [ { key: 'orgSettingsManage', label: 'Общие настройки' }, @@ -80,6 +112,8 @@ const PERM_GROUPS: { title: string; perms: { key: keyof RolePermissions; label: { key: 'rolesManage', label: 'Роли' }, { key: 'storesManage', label: 'Склады' }, { key: 'retailPointsManage', label: 'Кассы' }, + { key: 'cashRegistersManage', label: 'Кассовые аппараты (ККМ)' }, + { key: 'integrationsManage', label: 'Интеграции (МойСклад и т.п.)' }, ]}, ] @@ -87,6 +121,9 @@ 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 [pickTemplate, setPickTemplate] = useState(false) + const [templateId, setTemplateId] = useState('blank') const save = async () => { if (!form) return @@ -104,7 +141,7 @@ export function EmployeeRolesPage() { actions={ <> - @@ -142,6 +179,53 @@ export function EmployeeRolesPage() { /> + {/* Шаг выбора шаблона — открывается до основной модалки. */} + setPickTemplate(false)} + title="Новая роль — выберите шаблон" + footer={ + <> + + + + } + > +
+

+ Стартовый набор прав. После создания роли сможешь дополнительно настроить + каждый пункт через матрицу прав. +

+ + + {data?.items.map((r) => ( + + ))} +
+
+ setForm(null)} @@ -183,6 +267,7 @@ export function EmployeeRolesPage() { key={p.key} label={p.label} checked={form.permissions[p.key]} + disabled={form.isSystem} onChange={(v) => setForm({ ...form, permissions: { ...form.permissions, [p.key]: v } })} /> ))}