diff --git a/Distribution.Tests/Services/RegionServiceTests.cs b/Distribution.Tests/Services/RegionServiceTests.cs index 0b384cd..358cca7 100644 --- a/Distribution.Tests/Services/RegionServiceTests.cs +++ b/Distribution.Tests/Services/RegionServiceTests.cs @@ -1,4 +1,5 @@ using Distribution.Features.Region; +using Distribution.Infra.Authorization; using Distribution.Infra.UoW; using Distribution.Models; using Moq; @@ -10,11 +11,14 @@ public class RegionServiceTests { private readonly Mock _repo = new(); private readonly Mock _uow = new(); + private readonly Mock _currentUser = new(); + private readonly Mock _scopeAccess = new(); private readonly RegionService _sut; public RegionServiceTests() { - _sut = new RegionService(_repo.Object, _uow.Object); + _currentUser.Setup(u => u.RoleLevel).Returns(60); + _sut = new RegionService(_repo.Object, _uow.Object, _currentUser.Object, _scopeAccess.Object); } private static Region ActiveRegion(string name = "North") => diff --git a/Distribution/Data/AppDBContext.cs b/Distribution/Data/AppDBContext.cs index 94dd9de..79a4e39 100644 --- a/Distribution/Data/AppDBContext.cs +++ b/Distribution/Data/AppDBContext.cs @@ -56,6 +56,10 @@ public class AppDBContext(DbContextOptions options) : DbContext(op public DbSet Claims { get; set; } public DbSet SupplierClaims { get; set; } public DbSet SupplierClaimItems { get; set; } + public DbSet RegionalTargets { get; set; } + public DbSet RegionalBudgets { get; set; } + public DbSet RegionAgencyMaps { get; set; } + public DbSet RegionalTaxRules { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -373,6 +377,28 @@ public class AppDBContext(DbContextOptions options) : DbContext(op .HasForeignKey(t => t.FK_OrderId) .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity() + .HasIndex(t => new { t.FK_RegionId, t.Period }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(b => new { b.FK_RegionId, b.Period }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(m => new { m.FK_RegionId, m.FK_AgencyUserId }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(m => m.AgencyUser) + .WithMany() + .HasForeignKey(m => m.FK_AgencyUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(r => r.FK_RegionId) + .IsUnique(); + Seed.RoleAndPermissionSeed.Apply(modelBuilder); } } diff --git a/Distribution/Features/Region/IRegionRepository.cs b/Distribution/Features/Region/IRegionRepository.cs index 8eccc6f..43e0382 100644 --- a/Distribution/Features/Region/IRegionRepository.cs +++ b/Distribution/Features/Region/IRegionRepository.cs @@ -7,4 +7,26 @@ public interface IRegionRepository Task AddAsync(Models.Region entity); void Update(Models.Region entity); void Remove(Models.Region entity); + + Task GetTargetAsync(Guid regionId, string period); + Task AddTargetAsync(Models.RegionalTarget target); + void UpdateTarget(Models.RegionalTarget target); + + Task GetBudgetAsync(Guid regionId, string period); + Task AddBudgetAsync(Models.RegionalBudget budget); + void UpdateBudget(Models.RegionalBudget budget); + + Task GetAgencyMapAsync(Guid regionId, Guid agencyUserId); + Task AddAgencyMapAsync(Models.RegionAgencyMap map); + + Task GetTaxRuleAsync(Guid regionId); + Task AddTaxRuleAsync(Models.RegionalTaxRule rule); + void UpdateTaxRule(Models.RegionalTaxRule rule); + + Task GetUserRoleLevelAsync(Guid userId); + + Task> GetRoutesAsync(Guid regionId); + Task> GetStoresByRouteIdsAsync(List routeIds); + + Task> GetOrdersForDashboardAsync(Guid regionId, DateTime periodStart, DateTime periodEndExclusive); } diff --git a/Distribution/Features/Region/RegionController.cs b/Distribution/Features/Region/RegionController.cs index 61d33b8..7fe260a 100644 --- a/Distribution/Features/Region/RegionController.cs +++ b/Distribution/Features/Region/RegionController.cs @@ -37,4 +37,46 @@ public class RegionController(RegionService service) : ControllerBase var deleted = await service.DeleteAsync(id, ct); return deleted ? NoContent() : NotFound(); } + + [HttpPut("{id:guid}/targets/{period}")] + public async Task SetTarget(Guid id, string period, SetRegionalTargetRequest request, CancellationToken ct) + { + var result = await service.SetTargetAsync(id, period, request, ct); + return result is null ? NotFound() : Ok(result); + } + + [HttpPut("{id:guid}/budgets/{period}")] + public async Task SetBudget(Guid id, string period, SetRegionalBudgetRequest request, CancellationToken ct) + { + var result = await service.SetBudgetAsync(id, period, request, ct); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost("{id:guid}/agencies")] + public async Task MapAgency(Guid id, MapAgencyToRegionRequest request, CancellationToken ct) + { + var result = await service.MapAgencyAsync(id, request, ct); + return result is null ? NotFound() : Ok(result); + } + + [HttpPut("{id:guid}/tax-rule")] + public async Task SetTaxRule(Guid id, SetRegionalTaxRuleRequest request, CancellationToken ct) + { + var result = await service.SetTaxRuleAsync(id, request, ct); + return result is null ? NotFound() : Ok(result); + } + + [HttpGet("{id:guid}/hierarchy")] + public async Task GetHierarchy(Guid id) + { + var result = await service.GetHierarchyAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpGet("{id:guid}/dashboard")] + public async Task GetDashboard(Guid id, [FromQuery] string period) + { + var result = await service.GetDashboardAsync(id, period); + return result is null ? NotFound() : Ok(result); + } } diff --git a/Distribution/Features/Region/RegionDTOs.cs b/Distribution/Features/Region/RegionDTOs.cs index 828e5a3..08a1f5c 100644 --- a/Distribution/Features/Region/RegionDTOs.cs +++ b/Distribution/Features/Region/RegionDTOs.cs @@ -5,3 +5,32 @@ public record CreateRegionRequest(string Name); public record UpdateRegionRequest(string Name); public record RegionResponse(Guid Id, string Name, bool IsActive, DateTime CreatedAt); + +public record SetRegionalTargetRequest(decimal TargetValue); + +public record SetRegionalBudgetRequest(decimal BudgetAmount); + +public record MapAgencyToRegionRequest(Guid AgencyUserId); + +public record SetRegionalTaxRuleRequest(string RuleConfig); + +public record RegionalTargetResponse(Guid RegionId, string Period, decimal TargetValue); + +public record RegionalBudgetResponse(Guid RegionId, string Period, decimal BudgetAmount); + +public record RegionAgencyMapResponse(Guid RegionId, Guid AgencyUserId); + +public record RegionalTaxRuleResponse(Guid RegionId, string RuleConfig); + +public record RegionHierarchyRouteItem(Guid RouteId, string RouteName, List StoreIds); + +public record RegionHierarchyResponse(Guid RegionId, string RegionName, List Routes); + +public record RegionDashboardResponse( + Guid RegionId, + string Period, + decimal? TargetValue, + decimal? BudgetAmount, + int OrderCount, + decimal ActualSales, + DateTime? LastOrderDate); diff --git a/Distribution/Features/Region/RegionRepository.cs b/Distribution/Features/Region/RegionRepository.cs index f6f6efb..4e357e0 100644 --- a/Distribution/Features/Region/RegionRepository.cs +++ b/Distribution/Features/Region/RegionRepository.cs @@ -22,4 +22,60 @@ public class RegionRepository(AppDBContext context) : IRegionRepository entity.IsActive = false; context.Regions.Update(entity); } + + public async Task GetTargetAsync(Guid regionId, string period) => + await context.RegionalTargets.FirstOrDefaultAsync(t => + t.FK_RegionId == regionId && t.Period == period && t.IsActive); + + public async Task AddTargetAsync(Models.RegionalTarget target) => + await context.RegionalTargets.AddAsync(target); + + public void UpdateTarget(Models.RegionalTarget target) => + context.RegionalTargets.Update(target); + + public async Task GetBudgetAsync(Guid regionId, string period) => + await context.RegionalBudgets.FirstOrDefaultAsync(b => + b.FK_RegionId == regionId && b.Period == period && b.IsActive); + + public async Task AddBudgetAsync(Models.RegionalBudget budget) => + await context.RegionalBudgets.AddAsync(budget); + + public void UpdateBudget(Models.RegionalBudget budget) => + context.RegionalBudgets.Update(budget); + + public async Task GetAgencyMapAsync(Guid regionId, Guid agencyUserId) => + await context.RegionAgencyMaps.FirstOrDefaultAsync(m => + m.FK_RegionId == regionId && m.FK_AgencyUserId == agencyUserId && m.IsActive); + + public async Task AddAgencyMapAsync(Models.RegionAgencyMap map) => + await context.RegionAgencyMaps.AddAsync(map); + + public async Task GetTaxRuleAsync(Guid regionId) => + await context.RegionalTaxRules.FirstOrDefaultAsync(r => r.FK_RegionId == regionId && r.IsActive); + + public async Task AddTaxRuleAsync(Models.RegionalTaxRule rule) => + await context.RegionalTaxRules.AddAsync(rule); + + public void UpdateTaxRule(Models.RegionalTaxRule rule) => + context.RegionalTaxRules.Update(rule); + + public async Task GetUserRoleLevelAsync(Guid userId) => + await context.Users + .Where(u => u.Id == userId && u.IsActive) + .Select(u => (int?)u.Role.Level) + .FirstOrDefaultAsync(); + + public async Task> GetRoutesAsync(Guid regionId) => + await context.Routes.Where(r => r.FK_RegionId == regionId && r.IsActive).ToListAsync(); + + public async Task> GetStoresByRouteIdsAsync(List routeIds) => + await context.Stores.Where(s => routeIds.Contains(s.FK_RouteId) && s.IsActive).ToListAsync(); + + public async Task> GetOrdersForDashboardAsync(Guid regionId, DateTime periodStart, DateTime periodEndExclusive) => + await (from o in context.Orders + join s in context.Stores on o.FK_StoreId equals s.Id + join r in context.Routes on s.FK_RouteId equals r.Id + where r.FK_RegionId == regionId && o.IsActive && s.IsActive && r.IsActive + && o.OrderDate >= periodStart && o.OrderDate < periodEndExclusive + select o).ToListAsync(); } diff --git a/Distribution/Features/Region/RegionService.cs b/Distribution/Features/Region/RegionService.cs index 532b763..8a88406 100644 --- a/Distribution/Features/Region/RegionService.cs +++ b/Distribution/Features/Region/RegionService.cs @@ -1,9 +1,20 @@ +using Distribution.Infra.Authorization; using Distribution.Infra.UoW; namespace Distribution.Features.Region; -public class RegionService(IRegionRepository repo, IUnitOfWork uow) +public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess) { + private const int RegionalManagerRoleLevel = 50; + private const int RegionalAgencyRoleLevel = 60; + + private async Task CanManageRegionAsync(Guid regionId) + { + if (currentUser.RoleLevel >= RegionalAgencyRoleLevel) return true; + if (currentUser.RoleLevel < RegionalManagerRoleLevel || currentUser.UserId is null) return false; + return await scopeAccess.HasRegionScopeAsync(currentUser.UserId.Value, regionId); + } + public async Task> GetAllAsync() => (await repo.GetAllAsync()) .Where(r => r.IsActive) @@ -70,4 +81,161 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow) } return true; } + + public async Task SetTargetAsync(Guid regionId, string period, SetRegionalTargetRequest request, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + if (!await CanManageRegionAsync(regionId)) return null; + + var target = await repo.GetTargetAsync(regionId, period); + await uow.BeginAsync(ct); + try + { + if (target is null) + { + target = new Models.RegionalTarget { FK_RegionId = regionId, Period = period, TargetValue = request.TargetValue }; + await repo.AddTargetAsync(target); + } + else + { + target.TargetValue = request.TargetValue; + repo.UpdateTarget(target); + } + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return new RegionalTargetResponse(regionId, period, target.TargetValue); + } + + public async Task SetBudgetAsync(Guid regionId, string period, SetRegionalBudgetRequest request, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + if (!await CanManageRegionAsync(regionId)) return null; + + var budget = await repo.GetBudgetAsync(regionId, period); + await uow.BeginAsync(ct); + try + { + if (budget is null) + { + budget = new Models.RegionalBudget { FK_RegionId = regionId, Period = period, BudgetAmount = request.BudgetAmount }; + await repo.AddBudgetAsync(budget); + } + else + { + budget.BudgetAmount = request.BudgetAmount; + repo.UpdateBudget(budget); + } + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return new RegionalBudgetResponse(regionId, period, budget.BudgetAmount); + } + + public async Task MapAgencyAsync(Guid regionId, MapAgencyToRegionRequest request, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + if (!await CanManageRegionAsync(regionId)) return null; + + var agencyRoleLevel = await repo.GetUserRoleLevelAsync(request.AgencyUserId); + if (agencyRoleLevel is null || agencyRoleLevel < RegionalAgencyRoleLevel) return null; + + var existing = await repo.GetAgencyMapAsync(regionId, request.AgencyUserId); + if (existing is not null) return new RegionAgencyMapResponse(regionId, request.AgencyUserId); + + var map = new Models.RegionAgencyMap { FK_RegionId = regionId, FK_AgencyUserId = request.AgencyUserId }; + await uow.BeginAsync(ct); + try + { + await repo.AddAgencyMapAsync(map); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return new RegionAgencyMapResponse(regionId, request.AgencyUserId); + } + + public async Task SetTaxRuleAsync(Guid regionId, SetRegionalTaxRuleRequest request, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + if (!await CanManageRegionAsync(regionId)) return null; + + var rule = await repo.GetTaxRuleAsync(regionId); + await uow.BeginAsync(ct); + try + { + if (rule is null) + { + rule = new Models.RegionalTaxRule { FK_RegionId = regionId, RuleConfig = request.RuleConfig }; + await repo.AddTaxRuleAsync(rule); + } + else + { + rule.RuleConfig = request.RuleConfig; + repo.UpdateTaxRule(rule); + } + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return new RegionalTaxRuleResponse(regionId, rule.RuleConfig); + } + + public async Task GetHierarchyAsync(Guid regionId) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + + var routes = await repo.GetRoutesAsync(regionId); + var stores = await repo.GetStoresByRouteIdsAsync(routes.Select(r => r.Id).ToList()); + var routeItems = routes + .Select(r => new RegionHierarchyRouteItem( + r.Id, + r.RouteName, + stores.Where(s => s.FK_RouteId == r.Id).Select(s => s.Id).ToList())) + .ToList(); + return new RegionHierarchyResponse(region.Id, region.Name, routeItems); + } + + public async Task GetDashboardAsync(Guid regionId, string period) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + + if (!DateTime.TryParseExact(period, "yyyy-MM", null, System.Globalization.DateTimeStyles.None, out var periodStart)) + periodStart = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var normalizedPeriod = periodStart.ToString("yyyy-MM"); + var periodEndExclusive = periodStart.AddMonths(1); + + var target = await repo.GetTargetAsync(regionId, normalizedPeriod); + var budget = await repo.GetBudgetAsync(regionId, normalizedPeriod); + var orders = await repo.GetOrdersForDashboardAsync(regionId, periodStart, periodEndExclusive); + + return new RegionDashboardResponse( + regionId, + normalizedPeriod, + target?.TargetValue, + budget?.BudgetAmount, + orders.Count, + orders.Sum(o => o.TotalAmount), + orders.Count == 0 ? null : orders.Max(o => o.OrderDate)); + } } diff --git a/Distribution/Features/Route/RouteController.cs b/Distribution/Features/Route/RouteController.cs index 2813e29..f4ed704 100644 --- a/Distribution/Features/Route/RouteController.cs +++ b/Distribution/Features/Route/RouteController.cs @@ -31,6 +31,13 @@ public class RouteController(RouteService service) : ControllerBase return result is null ? NotFound() : Ok(result); } + [HttpPut("{id:guid}/region")] + public async Task AssignToRegion(Guid id, AssignRouteToRegionRequest request, CancellationToken ct) + { + var result = await service.AssignToRegionAsync(id, request, ct); + return result is null ? NotFound() : Ok(result); + } + [HttpDelete("{id:guid}")] public async Task Delete(Guid id, CancellationToken ct) { diff --git a/Distribution/Features/Route/RouteDTOs.cs b/Distribution/Features/Route/RouteDTOs.cs index 9ec9018..fb918d1 100644 --- a/Distribution/Features/Route/RouteDTOs.cs +++ b/Distribution/Features/Route/RouteDTOs.cs @@ -5,3 +5,5 @@ public record CreateRouteRequest(Guid RegionId, string RouteName, string StartPo public record UpdateRouteRequest(Guid RegionId, string RouteName, string StartPoint, string EndPoint, decimal Length); public record RouteResponse(Guid Id, Guid RegionId, string RouteName, string StartPoint, string EndPoint, decimal Length, bool IsActive, DateTime CreatedAt); + +public record AssignRouteToRegionRequest(Guid RegionId); diff --git a/Distribution/Features/Route/RouteService.cs b/Distribution/Features/Route/RouteService.cs index 0d41774..8fce029 100644 --- a/Distribution/Features/Route/RouteService.cs +++ b/Distribution/Features/Route/RouteService.cs @@ -2,7 +2,7 @@ using Distribution.Infra.UoW; namespace Distribution.Features.Route; -public class RouteService(IRouteRepository repo, IUnitOfWork uow) +public class RouteService(IRouteRepository repo, Region.IRegionRepository regionRepo, IUnitOfWork uow) { public async Task> GetAllAsync() => (await repo.GetAllAsync()) @@ -64,6 +64,29 @@ public class RouteService(IRouteRepository repo, IUnitOfWork uow) return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt); } + public async Task AssignToRegionAsync(Guid id, AssignRouteToRegionRequest request, CancellationToken ct = default) + { + var route = await repo.GetByIdAsync(id); + if (route is null || !route.IsActive) return null; + + var region = await regionRepo.GetByIdAsync(request.RegionId); + if (region is null || !region.IsActive) return null; + + route.FK_RegionId = request.RegionId; + await uow.BeginAsync(ct); + try + { + repo.Update(route); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt); + } + public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var route = await repo.GetByIdAsync(id); diff --git a/Distribution/Migrations/20260831050805_AddRegionManagement.Designer.cs b/Distribution/Migrations/20260831050805_AddRegionManagement.Designer.cs new file mode 100644 index 0000000..9ec81e1 --- /dev/null +++ b/Distribution/Migrations/20260831050805_AddRegionManagement.Designer.cs @@ -0,0 +1,2810 @@ +// +using System; +using Distribution.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Distribution.Migrations +{ + [DbContext(typeof(AppDBContext))] + [Migration("20260831050805_AddRegionManagement")] + partial class AddRegionManagement + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Distribution.Models.Brand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Brands"); + }); + + modelBuilder.Entity("Distribution.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowCustomerReturn") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("SettledAt") + .HasColumnType("datetime(6)"); + + b.Property("SettledByUserId") + .HasColumnType("char(36)"); + + b.Property("SettlementAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("SettledByUserId"); + + b.ToTable("Claims"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("CreditNotes"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_DriverProfileId") + .HasColumnType("char(36)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("FK_SourceWarehouseId") + .HasColumnType("char(36)"); + + b.Property("FK_VehicleId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Meterend") + .HasColumnType("int"); + + b.Property("Meterstart") + .HasColumnType("int"); + + b.Property("RideType") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_DriverProfileId"); + + b.HasIndex("FK_RouteId"); + + b.HasIndex("FK_SourceWarehouseId"); + + b.HasIndex("FK_VehicleId"); + + b.ToTable("DeliveryRuns"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CustomerConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_OrderId"); + + b.HasIndex("FK_RideId"); + + b.ToTable("DeliveryTasks"); + }); + + modelBuilder.Entity("Distribution.Models.DriverComplianceConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EnforceLicenseExpiry") + .HasColumnType("tinyint(1)"); + + b.Property("EnforceMedicalClearance") + .HasColumnType("tinyint(1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("DriverComplianceConfigs"); + }); + + modelBuilder.Entity("Distribution.Models.DriverProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LicenseExpiry") + .HasColumnType("datetime(6)"); + + b.Property("LicenseNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MedicalClearanceExpiry") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_UserId") + .IsUnique(); + + b.ToTable("DriverProfiles"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("FK_ReceivedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_WarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.HasIndex("FK_WarehouseId"); + + b.ToTable("GoodsReceipts"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcceptedQuantity") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RejectedQuantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_PurchaseOrderItemId"); + + b.ToTable("GoodsReceiptItems"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CostPrice") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ExternalErpItemId") + .HasColumnType("int"); + + b.Property("FK_BaseUomId") + .HasColumnType("char(36)"); + + b.Property("FK_BrandId") + .HasColumnType("char(36)"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("FK_SubCategoryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SalePrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SyncSource") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_BaseUomId"); + + b.HasIndex("FK_BrandId"); + + b.HasIndex("FK_CategoryId"); + + b.HasIndex("FK_SubCategoryId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DispatchConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("DispatchConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_SourceWarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceiptConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReceiptConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.HasIndex("FK_SourceWarehouseId"); + + b.ToTable("LoadingSheets"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("QuantityDispatched") + .HasColumnType("decimal(65,30)"); + + b.Property("QuantityReceived") + .HasColumnType("decimal(65,30)"); + + b.Property("QuantityResolved") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_LoadingSheetId"); + + b.ToTable("LoadingSheetItems"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfilledAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("OrderDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleId") + .IsUnique(); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Distribution.Models.OrderItem", b => + { + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("QuantityFulfilled") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("FK_OrderId", "FK_ItemId"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CollectedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollectedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Distribution.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveFrom") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveTo") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("PriceLists"); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PriceListId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("MinPrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PriceListId", "FK_ItemId") + .IsUnique(); + + b.ToTable("PriceListItems"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_SupplierId"); + + b.ToTable("PurchaseOrders"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledQuantity") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsCancelled") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("ReceivedQuantity") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.ToTable("PurchaseOrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.ToTable("PurchaseReturns"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturnItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PurchaseReturnId"); + + b.ToTable("PurchaseReturnItems"); + }); + + modelBuilder.Entity("Distribution.Models.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Regions"); + }); + + modelBuilder.Entity("Distribution.Models.RegionAgencyMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("FK_RegionId", "FK_AgencyUserId") + .IsUnique(); + + b.ToTable("RegionAgencyMaps"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BudgetAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalBudgets"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TargetValue") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalTargets"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RuleConfig") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId") + .IsUnique(); + + b.ToTable("RegionalTaxRules"); + }); + + modelBuilder.Entity("Distribution.Models.RideCancelRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RequestedAt") + .HasColumnType("datetime(6)"); + + b.Property("RequestedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TypeCode") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.ToTable("RideCancelRequests"); + }); + + modelBuilder.Entity("Distribution.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + + b.HasData( + new + { + Id = new Guid("10000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 10, + Name = "SalesRep" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 20, + Name = "Supervisor" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 30, + Name = "AreaManager" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 40, + Name = "AreaSupervisor" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 50, + Name = "RegionalManager" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 60, + Name = "RegionalAgency" + }); + }); + + modelBuilder.Entity("Distribution.Models.RolePermission", b => + { + b.Property("FK_RoleId") + .HasColumnType("char(36)"); + + b.Property("FK_PermissionId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_RoleId", "FK_PermissionId"); + + b.HasIndex("FK_PermissionId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("Distribution.Models.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Length") + .HasColumnType("decimal(65,30)"); + + b.Property("RouteName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Distribution.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfillImmediately") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("SaleDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FK_RideId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Mode") + .HasColumnType("int"); + + b.Property("TotalCreditAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FK_OrderId"); + + b.ToTable("SaleReturns"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("SaleReturnItems"); + }); + + modelBuilder.Entity("Distribution.Models.SaleRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("SaleRoutes"); + }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AgentLimitExempt") + .HasColumnType("tinyint(1)"); + + b.Property("BaselineExpectedSales") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeFrom") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeTo") + .HasColumnType("datetime(6)"); + + b.Property("EligibleSegment") + .HasColumnType("longtext"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("FK_TriggerItemId") + .HasColumnType("char(36)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RegionId") + .HasColumnType("char(36)"); + + b.Property("RuleConfig") + .HasColumnType("longtext"); + + b.Property("RuleType") + .HasColumnType("int"); + + b.Property("TriggerQty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_TriggerItemId"); + + b.HasIndex("RegionId"); + + b.ToTable("Schemes"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("Qty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SchemeId", "FK_ItemId") + .IsUnique(); + + b.ToTable("SchemeBundleComponents"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FieldInvoiceId") + .HasColumnType("char(36)"); + + b.Property("FreeItemCostValue") + .HasColumnType("decimal(65,30)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("RedeemedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_OrderId"); + + b.HasIndex("FK_SchemeId"); + + b.ToTable("SchemeRedemptions"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("LocationId") + .HasColumnType("char(36)"); + + b.Property("LocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "LocationId") + .IsUnique(); + + b.ToTable("StockBalances"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FromLocationId") + .HasColumnType("char(36)"); + + b.Property("FromLocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("longtext"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.Property("ToLocationId") + .HasColumnType("char(36)"); + + b.Property("ToLocationType") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("KycStatus") + .HasColumnType("int"); + + b.Property("Latitude") + .HasColumnType("decimal(65,30)"); + + b.Property("Longitude") + .HasColumnType("decimal(65,30)"); + + b.Property("OneTimeAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("OutstandingBalance") + .HasColumnType("decimal(65,30)"); + + b.Property("OwnerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Segment") + .HasColumnType("longtext"); + + b.Property("StoreAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RouteId"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("Distribution.Models.StoreKycDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FileReference") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("StoreKycDocuments"); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_CategoryId"); + + b.ToTable("SubCategories"); + }); + + modelBuilder.Entity("Distribution.Models.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactInfo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PerformanceScore") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EvidenceUrl") + .HasColumnType("longtext"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RaisedByUserId") + .HasColumnType("char(36)"); + + b.Property("ResolutionAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("ResolutionNotes") + .HasColumnType("longtext"); + + b.Property("ResolutionType") + .HasColumnType("int"); + + b.Property("ResolvedAt") + .HasColumnType("datetime(6)"); + + b.Property("ResolvedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.HasIndex("RaisedByUserId"); + + b.HasIndex("ResolvedByUserId"); + + b.ToTable("SupplierClaims"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierClaimId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssueType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SupplierClaimId"); + + b.ToTable("SupplierClaimItems"); + }); + + modelBuilder.Entity("Distribution.Models.Uom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Uoms"); + }); + + modelBuilder.Entity("Distribution.Models.UomConversion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_FromUomId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ToUomId") + .HasColumnType("char(36)"); + + b.Property("Factor") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_FromUomId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ToUomId"); + + b.ToTable("UomConversions"); + }); + + modelBuilder.Entity("Distribution.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ReportsToUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RoleId") + .HasColumnType("char(36)"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_ReportsToUserId"); + + b.HasIndex("FK_RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Distribution.Models.UserRegionScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_RegionId"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("UserRegionScopes"); + }); + + modelBuilder.Entity("Distribution.Models.UserRouteScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_RouteId"); + + b.HasIndex("FK_RouteId"); + + b.ToTable("UserRouteScopes"); + }); + + modelBuilder.Entity("Distribution.Models.UserStoreScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_StoreId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("UserStoreScopes"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DefaultWarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("NumberPlate") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RegNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DefaultWarehouseId"); + + b.ToTable("Vehicles"); + }); + + modelBuilder.Entity("Distribution.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Location") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("VehicleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("VehicleId"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "SettledByUser") + .WithMany() + .HasForeignKey("SettledByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AgencyUser"); + + b.Navigation("SettledByUser"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany() + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SaleReturn"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.HasOne("Distribution.Models.DriverProfile", "DriverProfile") + .WithMany() + .HasForeignKey("FK_DriverProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "SourceWarehouse") + .WithMany() + .HasForeignKey("FK_SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("FK_VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DriverProfile"); + + b.Navigation("Route"); + + b.Navigation("SourceWarehouse"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Ride"); + }); + + modelBuilder.Entity("Distribution.Models.DriverProfile", b => + { + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("FK_WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItem", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany("Items") + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseOrderItem", "PurchaseOrderItem") + .WithMany() + .HasForeignKey("FK_PurchaseOrderItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceipt"); + + b.Navigation("PurchaseOrderItem"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.HasOne("Distribution.Models.Uom", "BaseUom") + .WithMany() + .HasForeignKey("FK_BaseUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Brand", "Brand") + .WithMany() + .HasForeignKey("FK_BrandId"); + + b.HasOne("Distribution.Models.Category", "Category") + .WithMany() + .HasForeignKey("FK_CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.SubCategory", "SubCategory") + .WithMany() + .HasForeignKey("FK_SubCategoryId"); + + b.Navigation("BaseUom"); + + b.Navigation("Brand"); + + b.Navigation("Category"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "SourceWarehouse") + .WithMany() + .HasForeignKey("FK_SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ride"); + + b.Navigation("SourceWarehouse"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.LoadingSheet", "LoadingSheet") + .WithMany("Items") + .HasForeignKey("FK_LoadingSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("LoadingSheet"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.HasOne("Distribution.Models.Sale", "Sale") + .WithMany() + .HasForeignKey("FK_SaleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sale"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.OrderItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.HasOne("Distribution.Models.Sale", "Sale") + .WithMany() + .HasForeignKey("FK_SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sale"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.PriceList", "PriceList") + .WithMany("Items") + .HasForeignKey("FK_PriceListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PriceList"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.HasOne("Distribution.Models.Supplier", "Supplier") + .WithMany() + .HasForeignKey("FK_SupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrderItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany("Items") + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturnItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseReturn", "PurchaseReturn") + .WithMany("Items") + .HasForeignKey("FK_PurchaseReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseReturn"); + }); + + modelBuilder.Entity("Distribution.Models.RegionAgencyMap", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AgencyUser"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RideCancelRequest", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ride"); + }); + + modelBuilder.Entity("Distribution.Models.RolePermission", b => + { + b.HasOne("Distribution.Models.Permission", "Permission") + .WithMany() + .HasForeignKey("FK_PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Role", "Role") + .WithMany() + .HasForeignKey("FK_RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Distribution.Models.Route", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.Sale", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Ride"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany("Items") + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SaleReturn"); + }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Item", "TriggerItem") + .WithMany() + .HasForeignKey("FK_TriggerItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("RegionId"); + + b.Navigation("FreeItem"); + + b.Navigation("Region"); + + b.Navigation("TriggerItem"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("BundleComponents") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Scheme"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("Redemptions") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("FreeItem"); + + b.Navigation("Order"); + + b.Navigation("Scheme"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Distribution.Models.StoreKycDocument", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.HasOne("Distribution.Models.Category", "Category") + .WithMany() + .HasForeignKey("FK_CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany() + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "RaisedByUser") + .WithMany() + .HasForeignKey("RaisedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("GoodsReceipt"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("RaisedByUser"); + + b.Navigation("ResolvedByUser"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SupplierClaim", "SupplierClaim") + .WithMany("Items") + .HasForeignKey("FK_SupplierClaimId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SupplierClaim"); + }); + + modelBuilder.Entity("Distribution.Models.UomConversion", b => + { + b.HasOne("Distribution.Models.Uom", "FromUom") + .WithMany() + .HasForeignKey("FK_FromUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany("Conversions") + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Uom", "ToUom") + .WithMany() + .HasForeignKey("FK_ToUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FromUom"); + + b.Navigation("Item"); + + b.Navigation("ToUom"); + }); + + modelBuilder.Entity("Distribution.Models.User", b => + { + b.HasOne("Distribution.Models.User", "ReportsTo") + .WithMany() + .HasForeignKey("FK_ReportsToUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Role", "Role") + .WithMany() + .HasForeignKey("FK_RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ReportsTo"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Distribution.Models.UserRegionScope", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.UserRouteScope", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.UserStoreScope", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Store"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.HasOne("Distribution.Models.Warehouse", null) + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("Distribution.Models.Warehouse", b => + { + b.HasOne("Distribution.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("VehicleId"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Navigation("Conversions"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.Navigation("BundleComponents"); + + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Distribution/Migrations/20260831050805_AddRegionManagement.cs b/Distribution/Migrations/20260831050805_AddRegionManagement.cs new file mode 100644 index 0000000..ff51231 --- /dev/null +++ b/Distribution/Migrations/20260831050805_AddRegionManagement.cs @@ -0,0 +1,159 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + public partial class AddRegionManagement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RegionAgencyMaps", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_RegionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_AgencyUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RegionAgencyMaps", x => x.Id); + table.ForeignKey( + name: "FK_RegionAgencyMaps_Regions_FK_RegionId", + column: x => x.FK_RegionId, + principalTable: "Regions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RegionAgencyMaps_Users_FK_AgencyUserId", + column: x => x.FK_AgencyUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "RegionalBudgets", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_RegionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Period = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + BudgetAmount = table.Column(type: "decimal(65,30)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RegionalBudgets", x => x.Id); + table.ForeignKey( + name: "FK_RegionalBudgets_Regions_FK_RegionId", + column: x => x.FK_RegionId, + principalTable: "Regions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "RegionalTargets", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_RegionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Period = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TargetValue = table.Column(type: "decimal(65,30)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RegionalTargets", x => x.Id); + table.ForeignKey( + name: "FK_RegionalTargets_Regions_FK_RegionId", + column: x => x.FK_RegionId, + principalTable: "Regions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "RegionalTaxRules", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_RegionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + RuleConfig = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RegionalTaxRules", x => x.Id); + table.ForeignKey( + name: "FK_RegionalTaxRules_Regions_FK_RegionId", + column: x => x.FK_RegionId, + principalTable: "Regions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_RegionAgencyMaps_FK_AgencyUserId", + table: "RegionAgencyMaps", + column: "FK_AgencyUserId"); + + migrationBuilder.CreateIndex( + name: "IX_RegionAgencyMaps_FK_RegionId_FK_AgencyUserId", + table: "RegionAgencyMaps", + columns: new[] { "FK_RegionId", "FK_AgencyUserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RegionalBudgets_FK_RegionId_Period", + table: "RegionalBudgets", + columns: new[] { "FK_RegionId", "Period" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RegionalTargets_FK_RegionId_Period", + table: "RegionalTargets", + columns: new[] { "FK_RegionId", "Period" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RegionalTaxRules_FK_RegionId", + table: "RegionalTaxRules", + column: "FK_RegionId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RegionAgencyMaps"); + + migrationBuilder.DropTable( + name: "RegionalBudgets"); + + migrationBuilder.DropTable( + name: "RegionalTargets"); + + migrationBuilder.DropTable( + name: "RegionalTaxRules"); + } + } +} diff --git a/Distribution/Migrations/AppDBContextModelSnapshot.cs b/Distribution/Migrations/AppDBContextModelSnapshot.cs index ea26e45..da94a53 100644 --- a/Distribution/Migrations/AppDBContextModelSnapshot.cs +++ b/Distribution/Migrations/AppDBContextModelSnapshot.cs @@ -851,6 +851,121 @@ namespace Distribution.Migrations b.ToTable("Regions"); }); + modelBuilder.Entity("Distribution.Models.RegionAgencyMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("FK_RegionId", "FK_AgencyUserId") + .IsUnique(); + + b.ToTable("RegionAgencyMaps"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BudgetAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalBudgets"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TargetValue") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalTargets"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RuleConfig") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId") + .IsUnique(); + + b.ToTable("RegionalTaxRules"); + }); + modelBuilder.Entity("Distribution.Models.RideCancelRequest", b => { b.Property("Id") @@ -2185,6 +2300,58 @@ namespace Distribution.Migrations b.Navigation("PurchaseReturn"); }); + modelBuilder.Entity("Distribution.Models.RegionAgencyMap", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AgencyUser"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + modelBuilder.Entity("Distribution.Models.RideCancelRequest", b => { b.HasOne("Distribution.Models.DeliveryRun", "Ride") diff --git a/Distribution/Models/RegionAgencyMap.cs b/Distribution/Models/RegionAgencyMap.cs new file mode 100644 index 0000000..a91df42 --- /dev/null +++ b/Distribution/Models/RegionAgencyMap.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class RegionAgencyMap : Template + { + public Guid FK_RegionId { get; set; } + [ForeignKey("FK_RegionId")] + public Region Region { get; set; } + + /// User holding the RegionalAgency role that is mapped to this region. + public Guid FK_AgencyUserId { get; set; } + [ForeignKey("FK_AgencyUserId")] + public User AgencyUser { get; set; } + } +} diff --git a/Distribution/Models/RegionalBudget.cs b/Distribution/Models/RegionalBudget.cs new file mode 100644 index 0000000..3922b20 --- /dev/null +++ b/Distribution/Models/RegionalBudget.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class RegionalBudget : Template + { + public Guid FK_RegionId { get; set; } + [ForeignKey("FK_RegionId")] + public Region Region { get; set; } + + public string Period { get; set; } + public decimal BudgetAmount { get; set; } + } +} diff --git a/Distribution/Models/RegionalTarget.cs b/Distribution/Models/RegionalTarget.cs new file mode 100644 index 0000000..e5e6f82 --- /dev/null +++ b/Distribution/Models/RegionalTarget.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class RegionalTarget : Template + { + public Guid FK_RegionId { get; set; } + [ForeignKey("FK_RegionId")] + public Region Region { get; set; } + + public string Period { get; set; } + public decimal TargetValue { get; set; } + } +} diff --git a/Distribution/Models/RegionalTaxRule.cs b/Distribution/Models/RegionalTaxRule.cs new file mode 100644 index 0000000..84d2ab2 --- /dev/null +++ b/Distribution/Models/RegionalTaxRule.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class RegionalTaxRule : Template + { + public Guid FK_RegionId { get; set; } + [ForeignKey("FK_RegionId")] + public Region Region { get; set; } + + public string RuleConfig { get; set; } + } +} diff --git a/Distribution/docs/10_region_management.md b/Distribution/docs/10_region_management.md index 5ed01e5..4137265 100644 --- a/Distribution/docs/10_region_management.md +++ b/Distribution/docs/10_region_management.md @@ -1,6 +1,6 @@ # 10 — Region Management (Beyond CRUD) -**Status**: Not Started +**Status**: Implemented (except `assignAreaToRegion`, blocked on the `Area` entity) **Depends On**: 00 — Existing Gap Fixes --- @@ -24,48 +24,77 @@ CRUD: targets, budgets, agency mapping, tax rules, hierarchy, and dashboarding. ### Regional Target / Budget -`RegionId`, `Period` (e.g. `2026-08`), `TargetValue`, `BudgetAmount`. +Implemented as two separate entities, `RegionalTarget` and `RegionalBudget` +(`Models/RegionalTarget.cs`, `Models/RegionalBudget.cs`), each with `FK_RegionId`, `Period` +(string, `yyyy-MM`), and `TargetValue`/`BudgetAmount` respectively. A unique index on +`(FK_RegionId, Period)` means each region has at most one target and one budget row per period; +setting the same period again updates it in place rather than inserting a duplicate. ### Agency-Region Map -`RegionId`, `AgencyId` — depends on whether "Regional Agency" becomes a distinct entity or stays a -`Role` (see Open Questions). +Implemented as `RegionAgencyMap` (`Models/RegionAgencyMap.cs`): `FK_RegionId`, `FK_AgencyUserId`, +unique on the pair. "Regional Agency" stayed a `Role`, not a distinct entity (see Open Questions, +resolved) — `FK_AgencyUserId` points at an existing `User` who holds the `RegionalAgency` role, +the same convention already used by `Claim.FK_AgencyUserId` (§18). ### Regional Tax Rule -`RegionId`, `RuleConfig` (JSON) — deferred to whenever tax/pricing rule overrides are prioritized. +Implemented as `RegionalTaxRule` (`Models/RegionalTaxRule.cs`): `FK_RegionId` (unique — one rule +per region), `RuleConfig` (raw JSON string). Nothing in pricing/tax calculation reads this yet; +it's stored for a future pricing-rule integration. --- ## API Endpoints -| Function reference | Endpoint intent | -|---|---| -| `assignAreaToRegion(areaId, regionId)` | Blocked — no `Area` entity exists (see [00_existing_gaps.md](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)) | -| `setRegionalTarget(regionId, period, targetValue)` | `PUT /api/regions/{id}/targets/{period}` | -| `setRegionalBudget(regionId, period, budgetAmount)` | `PUT /api/regions/{id}/budgets/{period}` | -| `mapAgencyToRegion(agencyId, regionId)` | `POST /api/regions/{id}/agencies` | -| `setRegionalTaxRule(regionId, ruleConfig)` | `PUT /api/regions/{id}/tax-rule` | -| `getRegionHierarchy(regionId)` | `GET /api/regions/{id}/hierarchy` | -| `getRegionDashboard(regionId, period)` | `GET /api/regions/{id}/dashboard?period=...` | +| Function reference | Endpoint | Status | +|---|---|---| +| `assignAreaToRegion(areaId, regionId)` | — | Blocked — no `Area` entity exists (see [00_existing_gaps.md](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)). Stand-in: `PUT /api/routes/{id}/region` assigns/reassigns an existing route directly to a region (see [11_route_sales_management.md](./11_route_sales_management.md)). | +| `setRegionalTarget(regionId, period, targetValue)` | `PUT /api/regions/{id}/targets/{period}` | Implemented | +| `setRegionalBudget(regionId, period, budgetAmount)` | `PUT /api/regions/{id}/budgets/{period}` | Implemented | +| `mapAgencyToRegion(agencyId, regionId)` | `POST /api/regions/{id}/agencies` | Implemented | +| `setRegionalTaxRule(regionId, ruleConfig)` | `PUT /api/regions/{id}/tax-rule` | Implemented | +| `getRegionHierarchy(regionId)` | `GET /api/regions/{id}/hierarchy` | Implemented (region → routes → store ids; no `Area` level) | +| `getRegionDashboard(regionId, period)` | `GET /api/regions/{id}/dashboard?period=...` | Implemented (real-time aggregation; see Open Questions, resolved) | + +All request/response DTOs live in `Features/Region/RegionDTOs.cs`; routing and access checks are +in `RegionController.cs` and `RegionService.cs`. --- ## Business Rules - Only `RegionalManager` and above (per role level in `Data/Seed/RoleAndPermissionSeed.cs`) can - set targets/budgets for their own region; only `RegionalAgency` can set them across regions. + set targets/budgets/tax-rule for their own region; only `RegionalAgency` can set them across + regions. Implemented via `RegionService.CanManageRegionAsync`: role level ≥ 60 (`RegionalAgency`) + is allowed unconditionally, role level ≥ 50 (`RegionalManager`) requires a matching + `UserRegionScope` row for that region, anything lower is rejected. The same gate is applied to + `mapAgencyToRegion`. +- `mapAgencyToRegion` additionally rejects the target user if their role level is below the + `RegionalAgency` threshold (60) — you can only map a user who actually holds (or outranks) that + role. --- ## Open Questions -- Is "Regional Agency" a business entity distinct from the `RegionalAgency` role (e.g. a contracted - third party with its own record), or is the role sufficient? -- Does `getRegionDashboard` need real-time aggregation, or a scheduled/cached rollup? +- ~~Is "Regional Agency" a business entity distinct from the `RegionalAgency` role...?~~ Resolved: + kept as a `Role` on `User`; no new agency entity, consistent with `Claim.FK_AgencyUserId`. +- ~~Does `getRegionDashboard` need real-time aggregation, or a scheduled/cached rollup?~~ Resolved: + real-time aggregation from `Order`/`Store`/`Route` for now; revisit if/when region-wide dashboard + queries become a performance concern at scale. --- ## Implementation Notes -> Fill this section in during planning discussion before writing any code. +- Backend: `Models/RegionalTarget.cs`, `Models/RegionalBudget.cs`, `Models/RegionAgencyMap.cs`, + `Models/RegionalTaxRule.cs`, `Data/AppDBContext.cs` (DbSets + unique indexes), + `Features/Region/{IRegionRepository,RegionRepository,RegionService,RegionController,RegionDTOs}.cs`. +- Frontend: `distribution_fe/types/api.ts` (response types), `distribution_fe/lib/api/master.ts` + (`regions.setTarget/setBudget/mapAgency/setTaxRule/hierarchy/dashboard`), and a new + `RegionManageSheet` dialog (`distribution_fe/app/(dashboard)/network/regions/_components/region-manage-sheet.tsx`) + wired into the regions table's row actions. +- The EF Core migration `20260831050805_AddRegionManagement` has been generated and applied to the + shared database — `RegionalTargets`, `RegionalBudgets`, `RegionAgencyMaps`, and `RegionalTaxRules` + all exist. diff --git a/Distribution/docs/mainPlan.md b/Distribution/docs/mainPlan.md index 24f5d6e..0741239 100644 --- a/Distribution/docs/mainPlan.md +++ b/Distribution/docs/mainPlan.md @@ -54,7 +54,7 @@ Step 00 closed these. Kept here for history; all are fixed in the current codeba | 07 | Ride Close & Reconciliation | Not Started | 05, 06 | [07_ride_reconciliation.md](./07_ride_reconciliation.md) | | 08 | Sales Agent Controls | Not Started | 07 | [08_sales_agent_controls.md](./08_sales_agent_controls.md) | | 09 | Fraud Controls & Audit Trail | Not Started | 08 | [09_fraud_controls.md](./09_fraud_controls.md) | -| 10 | Region Management (Beyond CRUD) | Not Started | 00 | [10_region_management.md](./10_region_management.md) | +| 10 | Region Management (Beyond CRUD) | Done | 00 | [10_region_management.md](./10_region_management.md) | | 11 | Route & Sales Visit Management | Not Started | 00 | [11_route_sales_management.md](./11_route_sales_management.md) | | 12 | Store Management (Beyond CRUD) | Done | 00 | [12_store_management.md](./12_store_management.md) | | 13 | Fleet Management (Beyond CRUD) | Not Started | 00 | [13_fleet_management.md](./13_fleet_management.md) | @@ -116,19 +116,19 @@ here — see `SRS_User_Management_Module.pdf`. | Function | What it does | Status | |---|---|---| | `createRegion(name, boundaries)` | Creates a new region record. | Partially implemented — `RegionController` supports create with `Name` only, no `boundaries` field. | -| `assignAreaToRegion(areaId, regionId)` | Links an existing area under a region. | Not implemented — no `Area` entity exists in the schema. | -| `setRegionalTarget(regionId, period, targetValue)` | Sets the sales target for a region for a given period. | Not implemented. | -| `setRegionalBudget(regionId, period, budgetAmount)` | Allocates budget at the regional level. | Not implemented. | -| `mapAgencyToRegion(agencyId, regionId)` | Assigns a Regional Agency's contract scope to a region. | Not implemented — `RegionalAgency` is currently a `Role`, not a distinct agency entity; scope is via `UserRegionScope`. | -| `setRegionalTaxRule(regionId, ruleConfig)` | Overrides tax/pricing rules for a specific region. | Not implemented. | -| `getRegionHierarchy(regionId)` | Returns all areas/territories nested under a region. | Not implemented — see [user_management_PLAN.md](../user_management_PLAN.md) territory hierarchy prerequisite. | -| `getRegionDashboard(regionId, period)` | Aggregated KPIs: sales, fill rate, agency performance for the region. | Not implemented. | +| `assignAreaToRegion(areaId, regionId)` | Links an existing area under a region. | Not implemented — no `Area` entity exists in the schema (see [00_existing_gaps.md](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)). As a stand-in, a route (the next tier down) can be assigned/reassigned to a region directly via `PUT /api/routes/{id}/region`. | +| `setRegionalTarget(regionId, period, targetValue)` | Sets the sales target for a region for a given period. | Implemented — `PUT /api/regions/{id}/targets/{period}`, backed by the `RegionalTarget` entity. Restricted to `RegionalManager`+ with region scope, or `RegionalAgency` across any region. | +| `setRegionalBudget(regionId, period, budgetAmount)` | Allocates budget at the regional level. | Implemented — `PUT /api/regions/{id}/budgets/{period}`, backed by the `RegionalBudget` entity. Same access rule as `setRegionalTarget`. | +| `mapAgencyToRegion(agencyId, regionId)` | Assigns a Regional Agency's contract scope to a region. | Implemented — `POST /api/regions/{id}/agencies`, backed by the `RegionAgencyMap` entity. `RegionalAgency` is still a `Role`, not a distinct agency entity — `agencyId` is the `Guid` of an existing `User` holding that role (consistent with `Claim.FK_AgencyUserId`, §18); the endpoint rejects a user who doesn't hold the role. | +| `setRegionalTaxRule(regionId, ruleConfig)` | Overrides tax/pricing rules for a specific region. | Implemented — `PUT /api/regions/{id}/tax-rule`, backed by the `RegionalTaxRule` entity (`RuleConfig` stored as a raw JSON string, not yet consumed by pricing/tax logic elsewhere). | +| `getRegionHierarchy(regionId)` | Returns all areas/territories nested under a region. | Partially implemented — `GET /api/regions/{id}/hierarchy` returns the region's routes and each route's store ids; no `Area` level since that entity doesn't exist yet. | +| `getRegionDashboard(regionId, period)` | Aggregated KPIs: sales, fill rate, agency performance for the region. | Partially implemented — `GET /api/regions/{id}/dashboard?period=...` returns real-time order count, actual sales, target, and budget for the period; no fill-rate or agency-performance metrics yet. | ### 3. Route Management | Function | What it does | Status | |---|---|---| -| `createRoute(name, areaId)` | Creates a new sales visit route within an area. | Partially implemented — `RouteController` supports create, but by `RegionId` not `areaId` (no `Area` entity exists). | +| `createRoute(name, areaId)` | Creates a new sales visit route within an area. | Partially implemented — `RouteController` supports create, but by `RegionId` not `areaId` (no `Area` entity exists). A route's region can also be changed after creation via `PUT /api/routes/{id}/region`. | | `assignOutletsToRoute(routeId, outletIds[])` | Maps a set of stores to a route. | Partially implemented — a `Store` has a single `FK_RouteId`, set via `StoreController` create/update; no bulk-assign endpoint. | | `assignRouteToSalesRep(routeId, salesRepId)` | Assigns ownership of a route to an SR. | Not implemented — only `UserRouteScope` exists (scope grant), not a single-owner assignment. | | `optimizeRouteSequence(routeId)` | Reorders the visit sequence for efficiency (distance/priority-based). | Not implemented. | diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/region-manage-sheet.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/region-manage-sheet.tsx new file mode 100644 index 0000000..8d19d60 --- /dev/null +++ b/distribution_fe/app/(dashboard)/network/regions/_components/region-manage-sheet.tsx @@ -0,0 +1,276 @@ +'use client' + +import { useEffect, useState } from 'react' + +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog' +import { Separator } from '@/components/ui/separator' +import { toast } from '@/components/ui/toast' +import { regions as regionsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { + RegionResponse, + UserResponse, + RegionDashboardResponse, + RegionHierarchyResponse, +} from '@/types/api' + +interface RegionManageSheetProps { + open: boolean + onClose: () => void + region: RegionResponse | null +} + +function currentPeriod() { + const now = new Date() + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}` +} + +export function RegionManageSheet({ open, onClose, region }: RegionManageSheetProps) { + const [period, setPeriod] = useState(currentPeriod()) + const [targetValue, setTargetValue] = useState('') + const [budgetAmount, setBudgetAmount] = useState('') + const [ruleConfig, setRuleConfig] = useState('') + const [agencyUserId, setAgencyUserId] = useState('') + const [agencyUsers, setAgencyUsers] = useState([]) + const [dashboard, setDashboard] = useState(null) + const [hierarchy, setHierarchy] = useState(null) + const [saving, setSaving] = useState(null) + + useEffect(() => { + if (!open || !region) return + + setPeriod(currentPeriod()) + setTargetValue('') + setBudgetAmount('') + setRuleConfig('') + setAgencyUserId('') + + usersApi.list({ page: 1, pageSize: 200 }) + .then((res) => setAgencyUsers(getPaginatedItems(res).filter((u) => u.roleName === 'RegionalAgency'))) + .catch(() => toast.add({ title: 'Failed to load agency users', type: 'error' })) + + regionsApi.hierarchy(region.id) + .then(setHierarchy) + .catch(() => toast.add({ title: 'Failed to load hierarchy', type: 'error' })) + }, [open, region]) + + useEffect(() => { + if (!open || !region) return + regionsApi.dashboard(region.id, period) + .then(setDashboard) + .catch(() => toast.add({ title: 'Failed to load dashboard', type: 'error' })) + }, [open, region, period]) + + if (!region) return null + + async function handleSetTarget() { + const value = parseFloat(targetValue) + if (Number.isNaN(value)) return + setSaving('target') + try { + await regionsApi.setTarget(region!.id, period, value) + toast.add({ title: 'Target updated', type: 'success' }) + regionsApi.dashboard(region!.id, period).then(setDashboard) + } catch (err) { + toast.add({ title: 'Failed to set target', description: getApiError(err), type: 'error' }) + } finally { + setSaving(null) + } + } + + async function handleSetBudget() { + const value = parseFloat(budgetAmount) + if (Number.isNaN(value)) return + setSaving('budget') + try { + await regionsApi.setBudget(region!.id, period, value) + toast.add({ title: 'Budget updated', type: 'success' }) + regionsApi.dashboard(region!.id, period).then(setDashboard) + } catch (err) { + toast.add({ title: 'Failed to set budget', description: getApiError(err), type: 'error' }) + } finally { + setSaving(null) + } + } + + async function handleSetTaxRule() { + if (!ruleConfig.trim()) return + setSaving('tax-rule') + try { + await regionsApi.setTaxRule(region!.id, ruleConfig) + toast.add({ title: 'Tax rule updated', type: 'success' }) + } catch (err) { + toast.add({ title: 'Failed to set tax rule', description: getApiError(err), type: 'error' }) + } finally { + setSaving(null) + } + } + + async function handleMapAgency() { + if (!agencyUserId) return + setSaving('agency') + try { + await regionsApi.mapAgency(region!.id, agencyUserId) + toast.add({ title: 'Agency mapped to region', type: 'success' }) + setAgencyUserId('') + } catch (err) { + toast.add({ title: 'Failed to map agency', description: getApiError(err), type: 'error' }) + } finally { + setSaving(null) + } + } + + return ( + !v && onClose()}> + + + Manage {region.name} + + Targets, budgets, tax rules, agency mapping, and dashboard for this region. + + + +
+
+ + setPeriod(e.target.value)} + className="w-40" + /> +
+ + + +
+ +
+ setTargetValue(e.target.value)} + /> + +
+ {dashboard?.targetValue != null && ( +

Current: {dashboard.targetValue}

+ )} +
+ +
+ +
+ setBudgetAmount(e.target.value)} + /> + +
+ {dashboard?.budgetAmount != null && ( +

Current: {dashboard.budgetAmount}

+ )} +
+ + + +
+ +
+ setRuleConfig(e.target.value)} + /> + +
+
+ + + +
+ +
+ + +
+ {agencyUsers.length === 0 && ( +

No users with the RegionalAgency role found.

+ )} +
+ + + +
+ + {hierarchy && hierarchy.routes.length > 0 ? ( +
    + {hierarchy.routes.map((r) => ( +
  • + {r.routeName} — {r.storeIds.length} store{r.storeIds.length === 1 ? '' : 's'} +
  • + ))} +
+ ) : ( +

No routes under this region yet.

+ )} +
+ + + +
+ + {dashboard ? ( +
+
Orders: {dashboard.orderCount}
+
Actual Sales: {dashboard.actualSales}
+
Target: {dashboard.targetValue ?? '—'}
+
Budget: {dashboard.budgetAmount ?? '—'}
+
+ ) : ( +

Loading dashboard…

+ )} +
+
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx index 3017e36..2f23e80 100644 --- a/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx +++ b/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx @@ -13,6 +13,7 @@ import { regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api import type { RegionResponse } from '@/types/api' import { RegionSheet } from './region-sheet' +import { RegionManageSheet } from './region-manage-sheet' export function RegionsClient() { const [data, setData] = useState([]) @@ -24,6 +25,9 @@ export function RegionsClient() { const [sheetOpen, setSheetOpen] = useState(false) const [editing, setEditing] = useState(null) + const [manageOpen, setManageOpen] = useState(false) + const [managing, setManaging] = useState(null) + const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) try { @@ -51,6 +55,11 @@ export function RegionsClient() { setSheetOpen(true) } + function openManage(region: RegionResponse) { + setManaging(region) + setManageOpen(true) + } + async function toggleActive(region: RegionResponse) { try { await regionsApi.update(region.id, { isActive: !region.isActive }) @@ -103,6 +112,11 @@ export function RegionsClient() { ], rowActions: { actions: () => [ + { + action: 'confirm', + label: 'Manage targets, budgets & agencies', + onClick: openManage, + }, { action: 'edit', label: 'Edit region', @@ -122,7 +136,7 @@ export function RegionsClient() { }, ], }, - }), [pageSize, openEdit, toggleActive]) + }), [pageSize, openEdit, openManage, toggleActive]) return ( <> @@ -156,6 +170,12 @@ export function RegionsClient() { region={editing} onSuccess={() => fetchData(page, pageSize)} /> + + setManageOpen(false)} + region={managing} + /> ) } diff --git a/distribution_fe/lib/api/master.ts b/distribution_fe/lib/api/master.ts index 2cf514e..5fec4f2 100644 --- a/distribution_fe/lib/api/master.ts +++ b/distribution_fe/lib/api/master.ts @@ -2,6 +2,12 @@ import { api } from '@/lib/api' import type { PaginatedResponse, RegionResponse, + RegionalTargetResponse, + RegionalBudgetResponse, + RegionAgencyMapResponse, + RegionalTaxRuleResponse, + RegionHierarchyResponse, + RegionDashboardResponse, RouteResponse, VehicleResponse, WarehouseResponse, @@ -96,6 +102,24 @@ export const regions = { remove: (id: string) => api.delete(`/regions/${id}`), + + setTarget: (id: string, period: string, targetValue: number) => + api.put(`/regions/${id}/targets/${period}`, { targetValue }).then(r => r.data), + + setBudget: (id: string, period: string, budgetAmount: number) => + api.put(`/regions/${id}/budgets/${period}`, { budgetAmount }).then(r => r.data), + + mapAgency: (id: string, agencyUserId: string) => + api.post(`/regions/${id}/agencies`, { agencyUserId }).then(r => r.data), + + setTaxRule: (id: string, ruleConfig: string) => + api.put(`/regions/${id}/tax-rule`, { ruleConfig }).then(r => r.data), + + hierarchy: (id: string) => + api.get(`/regions/${id}/hierarchy`).then(r => r.data), + + dashboard: (id: string, period?: string) => + api.get(`/regions/${id}/dashboard`, { params: { period } }).then(r => r.data), } // ─── Routes ─────────────────────────────────────────────────────────────────── diff --git a/distribution_fe/types/api.ts b/distribution_fe/types/api.ts index 31f2162..8f6a317 100644 --- a/distribution_fe/types/api.ts +++ b/distribution_fe/types/api.ts @@ -85,6 +85,50 @@ export interface RouteResponse { createdAt: string } +export interface RegionalTargetResponse { + regionId: string + period: string + targetValue: number +} + +export interface RegionalBudgetResponse { + regionId: string + period: string + budgetAmount: number +} + +export interface RegionAgencyMapResponse { + regionId: string + agencyUserId: string +} + +export interface RegionalTaxRuleResponse { + regionId: string + ruleConfig: string +} + +export interface RegionHierarchyRouteItem { + routeId: string + routeName: string + storeIds: string[] +} + +export interface RegionHierarchyResponse { + regionId: string + regionName: string + routes: RegionHierarchyRouteItem[] +} + +export interface RegionDashboardResponse { + regionId: string + period: string + targetValue: number | null + budgetAmount: number | null + orderCount: number + actualSales: number + lastOrderDate: string | null +} + // ─── Vehicles / Warehouses ─────────────────────────────────────────────────── export interface VehicleResponse { diff --git a/sharedDocs/BACKEND_API_REFERENCE.md b/sharedDocs/BACKEND_API_REFERENCE.md index 31a58a4..6dddc51 100644 --- a/sharedDocs/BACKEND_API_REFERENCE.md +++ b/sharedDocs/BACKEND_API_REFERENCE.md @@ -506,6 +506,84 @@ Array of: } ``` +**Beyond CRUD** — targets, budgets, agency mapping, tax rules, hierarchy, and dashboard. These are +not gated by the `regions.*` permission codes above; access is checked by role level in +`RegionService`: `RegionalManager` (level 50) can act only on a region they have scope over (via +`UserRegionScope`), `RegionalAgency` (level 60) can act on any region. + +| Method | Path | Description | +|---|---|---| +| `PUT` | `/api/regions/{id}/targets/{period}` | Set the sales target for a region for a period (`period` = `yyyy-MM`) | +| `PUT` | `/api/regions/{id}/budgets/{period}` | Set the budget for a region for a period | +| `POST` | `/api/regions/{id}/agencies` | Map an existing user (holding the `RegionalAgency` role) to this region | +| `PUT` | `/api/regions/{id}/tax-rule` | Set/replace the region's tax rule config | +| `GET` | `/api/regions/{id}/hierarchy` | Region's routes and each route's store ids | +| `GET` | `/api/regions/{id}/dashboard?period=yyyy-MM` | Real-time order count/sales/target/budget for the period | + +**Set target request / response:** +```json +// PUT /api/regions/{id}/targets/{period} +{ "targetValue": 500000 } +``` +```json +{ "regionId": "guid", "period": "2026-08", "targetValue": 500000 } +``` + +**Set budget request / response:** +```json +// PUT /api/regions/{id}/budgets/{period} +{ "budgetAmount": 200000 } +``` +```json +{ "regionId": "guid", "period": "2026-08", "budgetAmount": 200000 } +``` + +**Map agency request / response:** +```json +// POST /api/regions/{id}/agencies +{ "agencyUserId": "guid" } // must be a User with the RegionalAgency role +``` +```json +{ "regionId": "guid", "agencyUserId": "guid" } +``` + +**Set tax rule request / response:** +```json +// PUT /api/regions/{id}/tax-rule +{ "ruleConfig": "{\"rate\": 0.15}" } // arbitrary JSON stored as a string; not yet consumed by pricing +``` +```json +{ "regionId": "guid", "ruleConfig": "{\"rate\": 0.15}" } +``` + +**Hierarchy response:** +```json +{ + "regionId": "guid", + "regionName": "string", + "routes": [ + { "routeId": "guid", "routeName": "string", "storeIds": ["guid", "..."] } + ] +} +``` + +**Dashboard response:** +```json +{ + "regionId": "guid", + "period": "2026-08", + "targetValue": 500000, // null if no target set for the period + "budgetAmount": 200000, // null if no budget set for the period + "orderCount": 42, + "actualSales": 187500.00, + "lastOrderDate": "datetime | null" +} +``` + +All four of these endpoints return `404` if the region doesn't exist, the caller lacks the +required region scope, or (for agency mapping) the target user doesn't hold the `RegionalAgency` +role — the response body carries no distinction between "not found" and "forbidden". + --- ### 6.6 Routes @@ -518,6 +596,7 @@ Array of: | `GET` | `/api/routes/{id}` | `routes.view` | Get route by ID | | `POST` | `/api/routes` | `routes.create` | Create route | | `PUT` | `/api/routes/{id}` | `routes.update` | Update route | +| `PUT` | `/api/routes/{id}/region` | `routes.update` | Assign/reassign the route to a region | | `DELETE` | `/api/routes/{id}` | `routes.delete` | Delete route | **Create/Update request:** @@ -545,6 +624,14 @@ Array of: } ``` +**Assign to region request:** +```json +// PUT /api/routes/{id}/region +{ "regionId": "guid" } +``` +Returns the same `RouteResponse` shape as above. `404` if the route or the target region doesn't +exist (or either is inactive). + --- ### 6.7 Vehicles