branch crud and intergration
This commit is contained in:
@@ -31,4 +31,8 @@ public sealed class CurrentUser : ICurrentUser
|
||||
}
|
||||
|
||||
public int AuditUserId => int.TryParse(UserId, out var id) ? id : User.SystemUserId;
|
||||
|
||||
public string? RoleCode => _accessor.HttpContext?.User?.FindFirstValue(AuthHexClaims.RoleCode);
|
||||
|
||||
public bool IsAdmin => string.Equals(RoleCode?.Trim(), "Admin", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -19,4 +19,14 @@ public interface ICurrentUser
|
||||
|
||||
/// <summary>True when the request carries an authenticated principal.</summary>
|
||||
bool IsAuthenticated { get; }
|
||||
|
||||
/// <summary>The AuthHex <c>RoleCode</c> claim verbatim, or null if absent/unauthenticated.</summary>
|
||||
string? RoleCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True when <see cref="RoleCode"/> is "Admin" (case-insensitive — AuthHex mints the claim
|
||||
/// independently of ERPCore's stored casing). Mirrors <c>RoleService.GetMeAsync</c>'s check;
|
||||
/// the only role that bypasses branch-scoping (see <see cref="Services.Interfaces.IBranchAccessService"/>).
|
||||
/// </summary>
|
||||
bool IsAdmin { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the singleton branch-configuration row. The check constraint is what makes
|
||||
/// "singleton" a database guarantee rather than a convention (mirrors ProductConfig).
|
||||
/// </summary>
|
||||
public sealed class BranchConfigConfiguration : IEntityTypeConfiguration<BranchConfig>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BranchConfig> builder)
|
||||
{
|
||||
builder.ToTable("branch_config", t =>
|
||||
t.HasCheckConstraint("ck_branch_config_singleton", $"\"ConfigId\" = {BranchConfig.SingletonId}"));
|
||||
|
||||
builder.HasKey(c => c.ConfigId);
|
||||
|
||||
// The id is fixed, never generated — there is exactly one row, seeded by DataSeeder.
|
||||
builder.Property(c => c.ConfigId).ValueGeneratedNever();
|
||||
|
||||
builder.Property(c => c.BranchingEnabled).IsRequired().HasDefaultValue(true);
|
||||
|
||||
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(c => c.UpdatedByUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.UpdatedBy)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public sealed class BranchConfiguration : IEntityTypeConfiguration<Branch>
|
||||
builder.HasIndex(b => b.Code).IsUnique();
|
||||
builder.Property(b => b.Name).IsRequired().HasMaxLength(200);
|
||||
builder.Property(b => b.Address).HasMaxLength(500);
|
||||
builder.Property(b => b.PhoneNumber).HasMaxLength(30);
|
||||
|
||||
builder.Property(b => b.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
|
||||
@@ -66,7 +66,8 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration<Permissio
|
||||
new Permission { PermissionId = 36, Code = "NAV:production.templates", SubNavItemId = 27 },
|
||||
new Permission { PermissionId = 37, Code = "NAV:production.runs", SubNavItemId = 25 },
|
||||
new Permission { PermissionId = 38, Code = "NAV:production.produced", SubNavItemId = 26 },
|
||||
new Permission { PermissionId = 39, Code = "NAV:procurement.purchase-invoices", SubNavItemId = 28 }
|
||||
new Permission { PermissionId = 39, Code = "NAV:procurement.purchase-invoices", SubNavItemId = 28 },
|
||||
new Permission { PermissionId = 40, Code = "NAV:settings.branches", SubNavItemId = 29 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
||||
new SubNavItem { SubNavItemId = 4, NavItemId = 2, Code = "products.item-type", Label = "Item Type", Href = "/dashboard/products/item-types", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 5, NavItemId = 2, Code = "products.uom", Label = "UOM", Href = "/dashboard/products/uoms", SortOrder = 5 },
|
||||
new SubNavItem { SubNavItemId = 6, NavItemId = 2, Code = "products.configuration", Label = "Configuration", Href = "/dashboard/products/settings", SortOrder = 6 },
|
||||
new SubNavItem { SubNavItemId = 29, NavItemId = 9, Code = "settings.branches", Label = "Branches", Href = "/dashboard/settings/branches", SortOrder = 0 },
|
||||
new SubNavItem { SubNavItemId = 7, NavItemId = 9, Code = "settings.roles", Label = "Roles", Href = "/dashboard/settings/roles", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 8, NavItemId = 9, Code = "settings.users", Label = "Users", Href = "/dashboard/settings/users", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 24, NavItemId = 9, Code = "settings.notification-config", Label = "Notification Config", Href = "/dashboard/settings/notification-config", SortOrder = 3 },
|
||||
|
||||
@@ -15,5 +15,9 @@ public sealed class WarehouseConfiguration : IEntityTypeConfiguration<Warehouse>
|
||||
builder.HasIndex(w => w.Code).IsUnique();
|
||||
|
||||
builder.Property(w => w.Name).IsRequired().HasMaxLength(200);
|
||||
|
||||
builder.HasOne(w => w.Branch).WithMany()
|
||||
.HasForeignKey(w => w.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasIndex(w => w.BranchId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public static class DataSeeder
|
||||
dirty |= await SeedItemTypesAsync(db, ct);
|
||||
// dirty |= await SeedCompanyProfileAsync(db, ct);
|
||||
dirty |= await SeedProductConfigAsync(db, ct);
|
||||
dirty |= await SeedBranchConfigAsync(db, ct);
|
||||
dirty |= await SeedNotificationConfigAsync(db, ct);
|
||||
dirty |= await SeedSalesMastersAsync(db, ct);
|
||||
dirty |= await SeedSalesStockAsync(db, ct);
|
||||
@@ -110,6 +111,23 @@ public static class DataSeeder
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the singleton branch-config row exists. New deployments start with branching
|
||||
/// on — today's mandatory-branch behaviour — so nothing changes for anyone until an
|
||||
/// admin deliberately switches it off from Settings.
|
||||
/// </summary>
|
||||
private static async Task<bool> SeedBranchConfigAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
if (await db.BranchConfig.AnyAsync(c => c.ConfigId == BranchConfig.SingletonId, ct)) return false;
|
||||
|
||||
db.BranchConfig.Add(new BranchConfig
|
||||
{
|
||||
ConfigId = BranchConfig.SingletonId,
|
||||
BranchingEnabled = true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> SeedNotificationConfigAsync(ErpDbContext db, CancellationToken ct)
|
||||
{
|
||||
if (await db.NotificationConfig.AnyAsync(c => c.NotificationConfigId == NotificationConfig.SingletonId, ct)) return false;
|
||||
|
||||
@@ -38,6 +38,8 @@ public class ErpDbContext : DbContext
|
||||
|
||||
/// <summary>Singleton row (FR-MD-11).</summary>
|
||||
public DbSet<ProductConfig> ProductConfig => Set<ProductConfig>();
|
||||
/// <summary>Singleton row gating whether this deployment uses branches at all.</summary>
|
||||
public DbSet<BranchConfig> BranchConfig => Set<BranchConfig>();
|
||||
/// <summary>Singleton row for notification delivery preferences.</summary>
|
||||
public DbSet<NotificationConfig> NotificationConfig => Set<NotificationConfig>();
|
||||
public DbSet<NotificationConfigModule> NotificationConfigModules => Set<NotificationConfigModule>();
|
||||
|
||||
Reference in New Issue
Block a user