add route visit management
This commit is contained in:
@@ -60,6 +60,7 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
|||||||
public DbSet<RegionalBudget> RegionalBudgets { get; set; }
|
public DbSet<RegionalBudget> RegionalBudgets { get; set; }
|
||||||
public DbSet<RegionAgencyMap> RegionAgencyMaps { get; set; }
|
public DbSet<RegionAgencyMap> RegionAgencyMaps { get; set; }
|
||||||
public DbSet<RegionalTaxRule> RegionalTaxRules { get; set; }
|
public DbSet<RegionalTaxRule> RegionalTaxRules { get; set; }
|
||||||
|
public DbSet<RouteVisitLog> RouteVisitLogs { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,4 +7,14 @@ public interface IRouteRepository
|
|||||||
Task AddAsync(Models.Route entity);
|
Task AddAsync(Models.Route entity);
|
||||||
void Update(Models.Route entity);
|
void Update(Models.Route entity);
|
||||||
void Remove(Models.Route entity);
|
void Remove(Models.Route entity);
|
||||||
|
|
||||||
|
Task<Models.User?> GetActiveUserAsync(Guid userId);
|
||||||
|
Task<List<Models.Store>> GetStoresByIdsAsync(List<Guid> storeIds);
|
||||||
|
Task<Models.Store?> GetStoreByIdAsync(Guid storeId);
|
||||||
|
void UpdateStore(Models.Store store);
|
||||||
|
|
||||||
|
Task<Models.RouteVisitLog?> GetOpenVisitAsync(Guid routeId, Guid storeId);
|
||||||
|
Task AddVisitLogAsync(Models.RouteVisitLog log);
|
||||||
|
void UpdateVisitLog(Models.RouteVisitLog log);
|
||||||
|
Task<List<Models.RouteVisitLog>> GetVisitsByStoreIdAsync(Guid storeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,36 @@ public class RouteController(RouteService service) : ControllerBase
|
|||||||
var deleted = await service.DeleteAsync(id, ct);
|
var deleted = await service.DeleteAsync(id, ct);
|
||||||
return deleted ? NoContent() : NotFound();
|
return deleted ? NoContent() : NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/stores")]
|
||||||
|
public async Task<IActionResult> AssignStores(Guid id, AssignStoresToRouteRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await service.AssignStoresToRouteAsync(id, request, ct);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/owner")]
|
||||||
|
public async Task<IActionResult> AssignOwner(Guid id, AssignRouteOwnerRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await service.AssignOwnerAsync(id, request, ct);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/optimize")]
|
||||||
|
public IActionResult OptimizeSequence(Guid id) =>
|
||||||
|
StatusCode(StatusCodes.Status501NotImplemented);
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/visits/check-in")]
|
||||||
|
public async Task<IActionResult> CheckIn(Guid id, RecordCheckInRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await service.RecordCheckInAsync(id, request, ct);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/visits/check-out")]
|
||||||
|
public async Task<IActionResult> CheckOut(Guid id, RecordCheckOutRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await service.RecordCheckOutAsync(id, request, ct);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ public record CreateRouteRequest(Guid RegionId, string RouteName, string StartPo
|
|||||||
|
|
||||||
public record UpdateRouteRequest(Guid RegionId, string RouteName, string StartPoint, string EndPoint, decimal Length);
|
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 RouteResponse(Guid Id, Guid RegionId, string RouteName, string StartPoint, string EndPoint, decimal Length, Guid? OwnerUserId, bool IsActive, DateTime CreatedAt);
|
||||||
|
|
||||||
public record AssignRouteToRegionRequest(Guid RegionId);
|
public record AssignRouteToRegionRequest(Guid RegionId);
|
||||||
|
|
||||||
|
public record AssignStoresToRouteRequest(List<Guid> StoreIds);
|
||||||
|
|
||||||
|
public record AssignRouteOwnerRequest(Guid SalesRepId);
|
||||||
|
|
||||||
|
public record RecordCheckInRequest(Guid StoreId, decimal Latitude, decimal Longitude, DateTime? Timestamp);
|
||||||
|
|
||||||
|
public record RecordCheckOutRequest(Guid StoreId, DateTime? Timestamp);
|
||||||
|
|
||||||
|
public record RouteVisitResponse(Guid Id, Guid RouteId, Guid StoreId, Guid UserId, DateTime CheckInAt, bool IsDeviationFlagged, DateTime? CheckOutAt);
|
||||||
|
|||||||
@@ -22,4 +22,31 @@ public class RouteRepository(AppDBContext context) : IRouteRepository
|
|||||||
entity.IsActive = false;
|
entity.IsActive = false;
|
||||||
context.Routes.Update(entity);
|
context.Routes.Update(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Models.User?> GetActiveUserAsync(Guid userId) =>
|
||||||
|
await context.Users.FirstOrDefaultAsync(u => u.Id == userId && u.IsActive);
|
||||||
|
|
||||||
|
public async Task<List<Models.Store>> GetStoresByIdsAsync(List<Guid> storeIds) =>
|
||||||
|
await context.Stores.Where(s => storeIds.Contains(s.Id)).ToListAsync();
|
||||||
|
|
||||||
|
public async Task<Models.Store?> GetStoreByIdAsync(Guid storeId) =>
|
||||||
|
await context.Stores.FindAsync(storeId);
|
||||||
|
|
||||||
|
public void UpdateStore(Models.Store store) =>
|
||||||
|
context.Stores.Update(store);
|
||||||
|
|
||||||
|
public async Task<Models.RouteVisitLog?> GetOpenVisitAsync(Guid routeId, Guid storeId) =>
|
||||||
|
await context.RouteVisitLogs
|
||||||
|
.Where(v => v.FK_RouteId == routeId && v.FK_StoreId == storeId && v.CheckOutAt == null)
|
||||||
|
.OrderByDescending(v => v.CheckInAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
public async Task AddVisitLogAsync(Models.RouteVisitLog log) =>
|
||||||
|
await context.RouteVisitLogs.AddAsync(log);
|
||||||
|
|
||||||
|
public void UpdateVisitLog(Models.RouteVisitLog log) =>
|
||||||
|
context.RouteVisitLogs.Update(log);
|
||||||
|
|
||||||
|
public async Task<List<Models.RouteVisitLog>> GetVisitsByStoreIdAsync(Guid storeId) =>
|
||||||
|
await context.RouteVisitLogs.Where(v => v.FK_StoreId == storeId).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,50 @@
|
|||||||
|
using Distribution.Infra.Authorization;
|
||||||
using Distribution.Infra.UoW;
|
using Distribution.Infra.UoW;
|
||||||
|
|
||||||
namespace Distribution.Features.Route;
|
namespace Distribution.Features.Route;
|
||||||
|
|
||||||
public class RouteService(IRouteRepository repo, Region.IRegionRepository regionRepo, IUnitOfWork uow)
|
public class RouteService(IRouteRepository repo, Region.IRegionRepository regionRepo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess)
|
||||||
{
|
{
|
||||||
|
private const int RouteManagerRoleLevel = 20;
|
||||||
|
private const double DeviationThresholdMeters = 500;
|
||||||
|
|
||||||
|
private async Task<bool> CanManageRouteAsync(Guid routeId)
|
||||||
|
{
|
||||||
|
if (currentUser.RoleLevel < RouteManagerRoleLevel || currentUser.UserId is null) return false;
|
||||||
|
return await scopeAccess.HasRouteScopeAsync(currentUser.UserId.Value, routeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double DistanceMeters(decimal lat1, decimal lon1, decimal lat2, decimal lon2)
|
||||||
|
{
|
||||||
|
const double earthRadiusMeters = 6371000;
|
||||||
|
var dLat = ToRadians((double)(lat2 - lat1));
|
||||||
|
var dLon = ToRadians((double)(lon2 - lon1));
|
||||||
|
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||||
|
Math.Cos(ToRadians((double)lat1)) * Math.Cos(ToRadians((double)lat2)) *
|
||||||
|
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||||
|
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||||
|
return earthRadiusMeters * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ToRadians(double degrees) => degrees * Math.PI / 180;
|
||||||
|
|
||||||
|
private static RouteResponse ToResponse(Models.Route route) =>
|
||||||
|
new(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.FK_OwnerUserId, route.IsActive, route.CreatedAt);
|
||||||
|
|
||||||
|
private static RouteVisitResponse ToVisitResponse(Models.RouteVisitLog log) =>
|
||||||
|
new(log.Id, log.FK_RouteId, log.FK_StoreId, log.FK_UserId, log.CheckInAt, log.IsDeviationFlagged, log.CheckOutAt);
|
||||||
|
|
||||||
public async Task<List<RouteResponse>> GetAllAsync() =>
|
public async Task<List<RouteResponse>> GetAllAsync() =>
|
||||||
(await repo.GetAllAsync())
|
(await repo.GetAllAsync())
|
||||||
.Where(r => r.IsActive)
|
.Where(r => r.IsActive)
|
||||||
.Select(r => new RouteResponse(r.Id, r.FK_RegionId, r.RouteName, r.StartPoint, r.EndPoint, r.Length, r.IsActive, r.CreatedAt))
|
.Select(r => new RouteResponse(r.Id, r.FK_RegionId, r.RouteName, r.StartPoint, r.EndPoint, r.Length, r.FK_OwnerUserId, r.IsActive, r.CreatedAt))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public async Task<RouteResponse?> GetByIdAsync(Guid id)
|
public async Task<RouteResponse?> GetByIdAsync(Guid id)
|
||||||
{
|
{
|
||||||
var route = await repo.GetByIdAsync(id);
|
var route = await repo.GetByIdAsync(id);
|
||||||
if (route is null || !route.IsActive) return null;
|
if (route is null || !route.IsActive) return null;
|
||||||
return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
|
return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.FK_OwnerUserId, route.IsActive, route.CreatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RouteResponse> CreateAsync(CreateRouteRequest request, CancellationToken ct = default)
|
public async Task<RouteResponse> CreateAsync(CreateRouteRequest request, CancellationToken ct = default)
|
||||||
@@ -38,7 +68,7 @@ public class RouteService(IRouteRepository repo, Region.IRegionRepository region
|
|||||||
await uow.RollbackAsync(ct);
|
await uow.RollbackAsync(ct);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
|
return ToResponse(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RouteResponse?> UpdateAsync(Guid id, UpdateRouteRequest request, CancellationToken ct = default)
|
public async Task<RouteResponse?> UpdateAsync(Guid id, UpdateRouteRequest request, CancellationToken ct = default)
|
||||||
@@ -61,7 +91,7 @@ public class RouteService(IRouteRepository repo, Region.IRegionRepository region
|
|||||||
await uow.RollbackAsync(ct);
|
await uow.RollbackAsync(ct);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
|
return ToResponse(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RouteResponse?> AssignToRegionAsync(Guid id, AssignRouteToRegionRequest request, CancellationToken ct = default)
|
public async Task<RouteResponse?> AssignToRegionAsync(Guid id, AssignRouteToRegionRequest request, CancellationToken ct = default)
|
||||||
@@ -84,7 +114,7 @@ public class RouteService(IRouteRepository repo, Region.IRegionRepository region
|
|||||||
await uow.RollbackAsync(ct);
|
await uow.RollbackAsync(ct);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
|
return ToResponse(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
||||||
@@ -104,4 +134,119 @@ public class RouteService(IRouteRepository repo, Region.IRegionRepository region
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<RouteResponse?> AssignStoresToRouteAsync(Guid id, AssignStoresToRouteRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var route = await repo.GetByIdAsync(id);
|
||||||
|
if (route is null || !route.IsActive) return null;
|
||||||
|
if (!await CanManageRouteAsync(id)) return null;
|
||||||
|
|
||||||
|
var stores = await repo.GetStoresByIdsAsync(request.StoreIds);
|
||||||
|
if (stores.Count != request.StoreIds.Distinct().Count()) return null;
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var store in stores)
|
||||||
|
{
|
||||||
|
store.FK_RouteId = id;
|
||||||
|
repo.UpdateStore(store);
|
||||||
|
}
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await uow.RollbackAsync(ct);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return ToResponse(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RouteResponse?> AssignOwnerAsync(Guid id, AssignRouteOwnerRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var route = await repo.GetByIdAsync(id);
|
||||||
|
if (route is null || !route.IsActive) return null;
|
||||||
|
if (!await CanManageRouteAsync(id)) return null;
|
||||||
|
|
||||||
|
var salesRep = await repo.GetActiveUserAsync(request.SalesRepId);
|
||||||
|
if (salesRep is null) return null;
|
||||||
|
|
||||||
|
route.FK_OwnerUserId = request.SalesRepId;
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
repo.Update(route);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await uow.RollbackAsync(ct);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return ToResponse(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RouteVisitResponse?> RecordCheckInAsync(Guid id, RecordCheckInRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (currentUser.UserId is null) return null;
|
||||||
|
|
||||||
|
var route = await repo.GetByIdAsync(id);
|
||||||
|
if (route is null || !route.IsActive) return null;
|
||||||
|
if (!await scopeAccess.HasRouteScopeAsync(currentUser.UserId.Value, id)) return null;
|
||||||
|
|
||||||
|
var store = await repo.GetStoreByIdAsync(request.StoreId);
|
||||||
|
if (store is null || !store.IsActive || store.FK_RouteId != id) return null;
|
||||||
|
|
||||||
|
var distance = DistanceMeters(request.Latitude, request.Longitude, store.Latitude, store.Longitude);
|
||||||
|
|
||||||
|
var log = new Models.RouteVisitLog
|
||||||
|
{
|
||||||
|
FK_RouteId = id,
|
||||||
|
FK_StoreId = request.StoreId,
|
||||||
|
FK_UserId = currentUser.UserId.Value,
|
||||||
|
CheckInAt = request.Timestamp ?? DateTime.Now,
|
||||||
|
CheckInLatitude = request.Latitude,
|
||||||
|
CheckInLongitude = request.Longitude,
|
||||||
|
IsDeviationFlagged = distance > DeviationThresholdMeters
|
||||||
|
};
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await repo.AddVisitLogAsync(log);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await uow.RollbackAsync(ct);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return ToVisitResponse(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RouteVisitResponse?> RecordCheckOutAsync(Guid id, RecordCheckOutRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (currentUser.UserId is null) return null;
|
||||||
|
|
||||||
|
var route = await repo.GetByIdAsync(id);
|
||||||
|
if (route is null || !route.IsActive) return null;
|
||||||
|
if (!await scopeAccess.HasRouteScopeAsync(currentUser.UserId.Value, id)) return null;
|
||||||
|
|
||||||
|
var log = await repo.GetOpenVisitAsync(id, request.StoreId);
|
||||||
|
if (log is null) return null;
|
||||||
|
|
||||||
|
log.CheckOutAt = request.Timestamp ?? DateTime.Now;
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
repo.UpdateVisitLog(log);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await uow.RollbackAsync(ct);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return ToVisitResponse(log);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Distribution.Models;
|
|||||||
|
|
||||||
namespace Distribution.Features.Store;
|
namespace Distribution.Features.Store;
|
||||||
|
|
||||||
public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, SchemeService schemeService)
|
public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, SchemeService schemeService, Route.IRouteRepository routeRepo)
|
||||||
{
|
{
|
||||||
private const int GlobalScopeRoleLevel = 50;
|
private const int GlobalScopeRoleLevel = 50;
|
||||||
private const int CreditApproverRoleLevel = 20;
|
private const int CreditApproverRoleLevel = 20;
|
||||||
@@ -209,8 +209,12 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
|||||||
var store = await repo.GetByIdAsync(id);
|
var store = await repo.GetByIdAsync(id);
|
||||||
if (store is null || !store.IsActive) return null;
|
if (store is null || !store.IsActive) return null;
|
||||||
if (!await HasStoreScopeAsync(id)) return null;
|
if (!await HasStoreScopeAsync(id)) return null;
|
||||||
// Wired to Step 11 (Route & Sales Management) once ride/visit tracking exists.
|
|
||||||
return [];
|
var visits = await routeRepo.GetVisitsByStoreIdAsync(id);
|
||||||
|
return visits
|
||||||
|
.OrderByDescending(v => v.CheckInAt)
|
||||||
|
.Select(v => new StoreVisitHistoryItem(v.Id, v.CheckInAt, v.FK_UserId, v.CheckOutAt is null ? "In Progress" : "Completed"))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SchemeEligibilityResponse?> CheckSchemeEligibilityAsync(Guid id, Guid schemeId)
|
public async Task<SchemeEligibilityResponse?> CheckSchemeEligibilityAsync(Guid id, Guid schemeId)
|
||||||
|
|||||||
+2895
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Distribution.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRouteVisitManagement : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "FK_OwnerUserId",
|
||||||
|
table: "Routes",
|
||||||
|
type: "char(36)",
|
||||||
|
nullable: true,
|
||||||
|
collation: "ascii_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "RouteVisitLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_RouteId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_StoreId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
CheckInAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
CheckInLatitude = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
|
||||||
|
CheckInLongitude = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
|
||||||
|
IsDeviationFlagged = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
CheckOutAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_RouteVisitLogs", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_RouteVisitLogs_Routes_FK_RouteId",
|
||||||
|
column: x => x.FK_RouteId,
|
||||||
|
principalTable: "Routes",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_RouteVisitLogs_Stores_FK_StoreId",
|
||||||
|
column: x => x.FK_StoreId,
|
||||||
|
principalTable: "Stores",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_RouteVisitLogs_Users_FK_UserId",
|
||||||
|
column: x => x.FK_UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Routes_FK_OwnerUserId",
|
||||||
|
table: "Routes",
|
||||||
|
column: "FK_OwnerUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RouteVisitLogs_FK_RouteId",
|
||||||
|
table: "RouteVisitLogs",
|
||||||
|
column: "FK_RouteId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RouteVisitLogs_FK_StoreId",
|
||||||
|
table: "RouteVisitLogs",
|
||||||
|
column: "FK_StoreId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RouteVisitLogs_FK_UserId",
|
||||||
|
table: "RouteVisitLogs",
|
||||||
|
column: "FK_UserId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Routes_Users_FK_OwnerUserId",
|
||||||
|
table: "Routes",
|
||||||
|
column: "FK_OwnerUserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Routes_Users_FK_OwnerUserId",
|
||||||
|
table: "Routes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "RouteVisitLogs");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Routes_FK_OwnerUserId",
|
||||||
|
table: "Routes");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FK_OwnerUserId",
|
||||||
|
table: "Routes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1115,6 +1115,9 @@ namespace Distribution.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FK_OwnerUserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<Guid>("FK_RegionId")
|
b.Property<Guid>("FK_RegionId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@@ -1134,11 +1137,60 @@ namespace Distribution.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FK_OwnerUserId");
|
||||||
|
|
||||||
b.HasIndex("FK_RegionId");
|
b.HasIndex("FK_RegionId");
|
||||||
|
|
||||||
b.ToTable("Routes");
|
b.ToTable("Routes");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.RouteVisitLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CheckInAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CheckInLatitude")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CheckInLongitude")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CheckOutAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_RouteId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_StoreId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_UserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeviationFlagged")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FK_RouteId");
|
||||||
|
|
||||||
|
b.HasIndex("FK_StoreId");
|
||||||
|
|
||||||
|
b.HasIndex("FK_UserId");
|
||||||
|
|
||||||
|
b.ToTable("RouteVisitLogs");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.Sale", b =>
|
modelBuilder.Entity("Distribution.Models.Sale", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2384,15 +2436,48 @@ namespace Distribution.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.Route", b =>
|
modelBuilder.Entity("Distribution.Models.Route", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("Distribution.Models.User", "OwnerUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_OwnerUserId");
|
||||||
|
|
||||||
b.HasOne("Distribution.Models.Region", "Region")
|
b.HasOne("Distribution.Models.Region", "Region")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("FK_RegionId")
|
.HasForeignKey("FK_RegionId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("OwnerUser");
|
||||||
|
|
||||||
b.Navigation("Region");
|
b.Navigation("Region");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.RouteVisitLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Distribution.Models.Route", "Route")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_RouteId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
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("Route");
|
||||||
|
|
||||||
|
b.Navigation("Store");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.Sale", b =>
|
modelBuilder.Entity("Distribution.Models.Sale", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Distribution.Models.User", "CreatedByUser")
|
b.HasOne("Distribution.Models.User", "CreatedByUser")
|
||||||
|
|||||||
@@ -12,5 +12,9 @@ namespace Distribution.Models
|
|||||||
public Guid FK_RegionId { get; set; }
|
public Guid FK_RegionId { get; set; }
|
||||||
[ForeignKey("FK_RegionId")]
|
[ForeignKey("FK_RegionId")]
|
||||||
public Region Region { get; set; }
|
public Region Region { get; set; }
|
||||||
|
|
||||||
|
public Guid? FK_OwnerUserId { get; set; }
|
||||||
|
[ForeignKey("FK_OwnerUserId")]
|
||||||
|
public User? OwnerUser { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Distribution.Models
|
||||||
|
{
|
||||||
|
public class RouteVisitLog : Template
|
||||||
|
{
|
||||||
|
public Guid FK_RouteId { get; set; }
|
||||||
|
[ForeignKey("FK_RouteId")]
|
||||||
|
public Route Route { get; set; }
|
||||||
|
|
||||||
|
public Guid FK_StoreId { get; set; }
|
||||||
|
[ForeignKey("FK_StoreId")]
|
||||||
|
public Store Store { get; set; }
|
||||||
|
|
||||||
|
public Guid FK_UserId { get; set; }
|
||||||
|
[ForeignKey("FK_UserId")]
|
||||||
|
public User User { get; set; }
|
||||||
|
|
||||||
|
public DateTime CheckInAt { get; set; }
|
||||||
|
public decimal CheckInLatitude { get; set; }
|
||||||
|
public decimal CheckInLongitude { get; set; }
|
||||||
|
public bool IsDeviationFlagged { get; set; } = false;
|
||||||
|
|
||||||
|
public DateTime? CheckOutAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user