68b34b3fc3
Refactored store creation and editing from modal sheets to dedicated pages (`/stores/new`, `/stores/[id]/edit`), and added a new store detail view page (`/stores/[id]`). Introduced a `LocationPicker` component using `react-leaflet` for map-based location selection, replacing manual latitude/longitude inputs. Updated navigation in the store list to use these pages. Improved UI and user feedback for store forms and details. Updated backend API and form logic to match the new flow. Added `leaflet`, `react-leaflet`, and types as dependencies. Temporarily relaxed store scope checks in `StoreService.cs`. Updated table actions and types for the new workflow.
250 lines
8.9 KiB
C#
250 lines
8.9 KiB
C#
using Distribution.Features.Scheme;
|
|
using Distribution.Infra.Authorization;
|
|
using Distribution.Infra.UoW;
|
|
using Distribution.Models;
|
|
|
|
namespace Distribution.Features.Store;
|
|
|
|
public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, SchemeService schemeService, Route.IRouteRepository routeRepo)
|
|
{
|
|
private const int GlobalScopeRoleLevel = 50;
|
|
private const int CreditApproverRoleLevel = 20;
|
|
|
|
private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel;
|
|
|
|
private async Task<bool> HasStoreScopeAsync(Guid storeId)
|
|
{
|
|
//if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope;
|
|
return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId);
|
|
}
|
|
|
|
private static StoreResponse ToResponse(Models.Store s) =>
|
|
new(s.Id, s.FK_RouteId, s.OwnerName, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount,
|
|
s.CreditLimit, s.OutstandingBalance, s.Segment, s.KycStatus, s.IsActive, s.CreatedAt);
|
|
|
|
public async Task<List<StoreResponse>> GetAllAsync()
|
|
{
|
|
var stores = (await repo.GetAllAsync()).Where(s => s.IsActive).ToList();
|
|
if (!HasGlobalScope && currentUser.UserId is not null)
|
|
{
|
|
var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet();
|
|
stores = stores.Where(s => accessibleStoreIds.Contains(s.Id)).ToList();
|
|
}
|
|
return stores.Select(ToResponse).ToList();
|
|
}
|
|
|
|
public async Task<StoreResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
//if (!await HasStoreScopeAsync(id)) return null;
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse> CreateAsync(CreateStoreRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = new Models.Store
|
|
{
|
|
FK_RouteId = request.RouteId,
|
|
OwnerName = request.OwnerName,
|
|
StoreName = request.StoreName,
|
|
StoreAddress = request.StoreAddress,
|
|
Latitude = request.Latitude,
|
|
Longitude = request.Longitude,
|
|
OneTimeAmount = request.OneTimeAmount,
|
|
CreditAmount = request.CreditAmount
|
|
};
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await repo.AddAsync(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse?> UpdateAsync(Guid id, UpdateStoreRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.FK_RouteId = request.RouteId;
|
|
store.OwnerName = request.OwnerName;
|
|
store.StoreName = request.StoreName;
|
|
store.StoreAddress = request.StoreAddress;
|
|
store.Latitude = request.Latitude;
|
|
store.Longitude = request.Longitude;
|
|
store.OneTimeAmount = request.OneTimeAmount;
|
|
store.CreditAmount = request.CreditAmount;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return false;
|
|
if (!await HasStoreScopeAsync(id)) return false;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Remove(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public async Task<StoreResponse?> UpdateCreditLimitAsync(Guid id, UpdateCreditLimitRequest request, CancellationToken ct = default)
|
|
{
|
|
if (currentUser.RoleLevel < CreditApproverRoleLevel) return null;
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.CreditLimit = request.NewLimit;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse?> UpdateOutstandingBalanceAsync(Guid id, UpdateOutstandingBalanceRequest request, CancellationToken ct = default)
|
|
{
|
|
if (currentUser.RoleLevel < CreditApproverRoleLevel) return null;
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.OutstandingBalance = request.NewBalance;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<OutstandingBalanceResponse?> GetOutstandingBalanceAsync(Guid id)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
return new OutstandingBalanceResponse(store.Id, store.CreditLimit, store.OutstandingBalance, store.CreditLimit - store.OutstandingBalance);
|
|
}
|
|
|
|
public async Task<StoreResponse?> VerifyKycAsync(Guid id, VerifyKycRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.KycStatus = request.Status;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse?> ClassifySegmentAsync(Guid id, ClassifySegmentRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.Segment = request.Segment;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<List<StoreVisitHistoryItem>?> GetVisitHistoryAsync(Guid id)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
|
|
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)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
|
|
var result = await schemeService.CheckStoreScopeEligibilityAsync(id, schemeId);
|
|
if (result is null) return null;
|
|
return new SchemeEligibilityResponse(result.Value.Eligible, result.Value.Reason);
|
|
}
|
|
|
|
public async Task<StoreDashboardResponse?> GetDashboardAsync(Guid id, string period)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) 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 periodEndExclusive = periodStart.AddMonths(1);
|
|
|
|
var orders = await repo.GetOrdersForDashboardAsync(id, periodStart, periodEndExclusive);
|
|
return new StoreDashboardResponse(
|
|
id,
|
|
periodStart.ToString("yyyy-MM"),
|
|
orders.Count,
|
|
orders.Sum(o => o.TotalAmount),
|
|
orders.Count == 0 ? null : orders.Max(o => o.OrderDate));
|
|
}
|
|
}
|