Базовый каркас модуля «Сотрудники и Роли» (по образу сторонняя система):
Domain:
- Employee — сотрудник организации (UserId nullable: запись может
существовать без логина), ФИО + Position + Email/Phone + Role + IsActive
+ FiredAt + RetailPointAssignments.
- EmployeeRole — роль с IsSystem флагом и owned RolePermissions.
- RolePermissions — 21 булев флаг по группам (Каталог/Закупки/Продажи/
Контрагенты/Отчёты/Настройки) + helper All() для админа.
- EmployeeRetailPointAssignment — ассоциация сотрудника с RetailPoint
(для роли Кассир — к каким кассам привязан).
Infrastructure:
- OrganizationsHrConfigurations с OwnsOne(...).ToJson("permissions")
для permissions — JSONB-колонка вместо отдельной таблицы.
- DbSet<EmployeeRole/Employee/EmployeeRetailPointAssignment>.
- Уникальные индексы: (OrgId, RoleName), (OrgId, UserId) с filter
WHERE UserId IS NOT NULL, (EmployeeId, RetailPointId).
Migration Phase4_EmployeesAndRoles создаёт три таблицы. Сидер
системных ролей и привязка существующего admin'а к Employee —
следующим коммитом, контроллеры и UI — далее.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
45 lines
2 KiB
C#
45 lines
2 KiB
C#
using foodmarket.Domain.Organizations;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace foodmarket.Infrastructure.Persistence.Configurations;
|
||
|
||
public static class OrganizationsHrConfigurations
|
||
{
|
||
public static void ConfigureOrganizationsHr(this ModelBuilder b)
|
||
{
|
||
b.Entity<EmployeeRole>(e =>
|
||
{
|
||
e.ToTable("employee_roles");
|
||
e.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
||
e.Property(x => x.Description).HasMaxLength(500);
|
||
// Permissions — owned JSON-колонка. Один объект = один сериализованный
|
||
// блоб на роль. Все поля булевы, по умолчанию false.
|
||
e.OwnsOne(x => x.Permissions, p => p.ToJson("permissions"));
|
||
e.HasIndex(x => new { x.OrganizationId, x.Name }).IsUnique();
|
||
});
|
||
|
||
b.Entity<Employee>(e =>
|
||
{
|
||
e.ToTable("employees");
|
||
e.Property(x => x.LastName).HasMaxLength(100).IsRequired();
|
||
e.Property(x => x.FirstName).HasMaxLength(100).IsRequired();
|
||
e.Property(x => x.MiddleName).HasMaxLength(100);
|
||
e.Property(x => x.Position).HasMaxLength(150);
|
||
e.Property(x => x.Email).HasMaxLength(200);
|
||
e.Property(x => x.Phone).HasMaxLength(50);
|
||
e.HasOne(x => x.Role).WithMany().HasForeignKey(x => x.RoleId).OnDelete(DeleteBehavior.Restrict);
|
||
e.HasMany(x => x.RetailPointAssignments).WithOne(a => a.Employee)
|
||
.HasForeignKey(a => a.EmployeeId).OnDelete(DeleteBehavior.Cascade);
|
||
e.HasIndex(x => new { x.OrganizationId, x.UserId }).IsUnique()
|
||
.HasFilter("\"UserId\" IS NOT NULL");
|
||
e.HasIndex(x => new { x.OrganizationId, x.LastName });
|
||
});
|
||
|
||
b.Entity<EmployeeRetailPointAssignment>(e =>
|
||
{
|
||
e.ToTable("employee_retail_point_assignments");
|
||
e.HasIndex(x => new { x.EmployeeId, x.RetailPointId }).IsUnique();
|
||
});
|
||
}
|
||
}
|