Added the currency master table
This commit is contained in:
parent
d28f37714f
commit
1a0641162c
@ -53,6 +53,7 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
public DbSet<Module> Modules { get; set; }
|
public DbSet<Module> Modules { get; set; }
|
||||||
public DbSet<Feature> Features { get; set; }
|
public DbSet<Feature> Features { get; set; }
|
||||||
public DbSet<FeaturePermission> FeaturePermissions { get; set; }
|
public DbSet<FeaturePermission> FeaturePermissions { get; set; }
|
||||||
|
public DbSet<CurrencyMaster> CurrencyMaster { get; set; }
|
||||||
public DbSet<ApplicationRole> ApplicationRoles { get; set; }
|
public DbSet<ApplicationRole> ApplicationRoles { get; set; }
|
||||||
public DbSet<JobRole> JobRoles { get; set; }
|
public DbSet<JobRole> JobRoles { get; set; }
|
||||||
public DbSet<RolePermissionMappings> RolePermissionMappings { get; set; }
|
public DbSet<RolePermissionMappings> RolePermissionMappings { get; set; }
|
||||||
@ -760,6 +761,65 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
new FeaturePermission { Id = new Guid("bdee29a2-b73b-402d-8dd1-c4b1f81ccbc3"), FeatureId = new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), IsEnabled = true, Name = "Manage", Description = "Allows a user to configure and control system settings, such as managing expense types, payment modes, permissions, and overall workflow rules." }
|
new FeaturePermission { Id = new Guid("bdee29a2-b73b-402d-8dd1-c4b1f81ccbc3"), FeatureId = new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), IsEnabled = true, Name = "Manage", Description = "Allows a user to configure and control system settings, such as managing expense types, payment modes, permissions, and overall workflow rules." }
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
modelBuilder.Entity<CurrencyMaster>().HasData(
|
||||||
|
new CurrencyMaster
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("78e96e4a-7ce0-4164-ae3a-c833ad45ec2c"),
|
||||||
|
CurrencyCode = "INR",
|
||||||
|
CurrencyName = "Indian Rupee",
|
||||||
|
Symbol = "₹",
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new CurrencyMaster
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("2f672568-a67b-4961-acb2-a8c7834e1762"),
|
||||||
|
CurrencyCode = "USD",
|
||||||
|
CurrencyName = "US Dollar",
|
||||||
|
Symbol = "$",
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new CurrencyMaster
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("4d1155bb-1448-4d97-a732-96c92eb99c45"),
|
||||||
|
CurrencyCode = "EUR",
|
||||||
|
CurrencyName = "Euro",
|
||||||
|
Symbol = "€",
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new CurrencyMaster
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("3e456237-ef06-4ea1-a261-188c9b0c6df6"),
|
||||||
|
CurrencyCode = "GBP",
|
||||||
|
CurrencyName = "Pound Sterling",
|
||||||
|
Symbol = "£",
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new CurrencyMaster
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("297e237a-56d3-48f6-b39d-ec3991dea8bf"),
|
||||||
|
CurrencyCode = "JPY",
|
||||||
|
CurrencyName = "Japanese Yen",
|
||||||
|
Symbol = "¥",
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new CurrencyMaster
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("efe9b4f6-64d6-446e-a42d-1c7aaf6dd70d"),
|
||||||
|
CurrencyCode = "RUB",
|
||||||
|
CurrencyName = "Russian Ruble",
|
||||||
|
Symbol = "₽",
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new CurrencyMaster
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("b960166a-f7e9-49e3-bb4b-28511f126c08"),
|
||||||
|
CurrencyCode = "CNY",
|
||||||
|
CurrencyName = "Chinese Yuan (Renminbi)",
|
||||||
|
Symbol = "¥",
|
||||||
|
IsActive = true
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4430
Marco.Pms.DataAccess/Migrations/20250730070549_Added_CurrencyMaster_Table.Designer.cs
generated
Normal file
4430
Marco.Pms.DataAccess/Migrations/20250730070549_Added_CurrencyMaster_Table.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace Marco.Pms.DataAccess.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Added_CurrencyMaster_Table : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CurrencyMaster",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
CurrencyCode = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CurrencyName = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Symbol = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CurrencyMaster", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "CurrencyMaster",
|
||||||
|
columns: new[] { "Id", "CurrencyCode", "CurrencyName", "IsActive", "Symbol" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("297e237a-56d3-48f6-b39d-ec3991dea8bf"), "JPY", "Japanese Yen", true, "¥" },
|
||||||
|
{ new Guid("2f672568-a67b-4961-acb2-a8c7834e1762"), "USD", "US Dollar", true, "$" },
|
||||||
|
{ new Guid("3e456237-ef06-4ea1-a261-188c9b0c6df6"), "GBP", "Pound Sterling", true, "£" },
|
||||||
|
{ new Guid("4d1155bb-1448-4d97-a732-96c92eb99c45"), "EUR", "Euro", true, "€" },
|
||||||
|
{ new Guid("78e96e4a-7ce0-4164-ae3a-c833ad45ec2c"), "INR", "Indian Rupee", true, "₹" },
|
||||||
|
{ new Guid("b960166a-f7e9-49e3-bb4b-28511f126c08"), "CNY", "Chinese Yuan (Renminbi)", true, "¥" },
|
||||||
|
{ new Guid("efe9b4f6-64d6-446e-a42d-1c7aaf6dd70d"), "RUB", "Russian Ruble", true, "₽" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CurrencyMaster");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1901,6 +1901,90 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.ToTable("ActivityMasters");
|
b.ToTable("ActivityMasters");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Marco.Pms.Model.Master.CurrencyMaster", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("CurrencyMaster");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("78e96e4a-7ce0-4164-ae3a-c833ad45ec2c"),
|
||||||
|
CurrencyCode = "INR",
|
||||||
|
CurrencyName = "Indian Rupee",
|
||||||
|
IsActive = true,
|
||||||
|
Symbol = "₹"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("2f672568-a67b-4961-acb2-a8c7834e1762"),
|
||||||
|
CurrencyCode = "USD",
|
||||||
|
CurrencyName = "US Dollar",
|
||||||
|
IsActive = true,
|
||||||
|
Symbol = "$"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("4d1155bb-1448-4d97-a732-96c92eb99c45"),
|
||||||
|
CurrencyCode = "EUR",
|
||||||
|
CurrencyName = "Euro",
|
||||||
|
IsActive = true,
|
||||||
|
Symbol = "€"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("3e456237-ef06-4ea1-a261-188c9b0c6df6"),
|
||||||
|
CurrencyCode = "GBP",
|
||||||
|
CurrencyName = "Pound Sterling",
|
||||||
|
IsActive = true,
|
||||||
|
Symbol = "£"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("297e237a-56d3-48f6-b39d-ec3991dea8bf"),
|
||||||
|
CurrencyCode = "JPY",
|
||||||
|
CurrencyName = "Japanese Yen",
|
||||||
|
IsActive = true,
|
||||||
|
Symbol = "¥"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("efe9b4f6-64d6-446e-a42d-1c7aaf6dd70d"),
|
||||||
|
CurrencyCode = "RUB",
|
||||||
|
CurrencyName = "Russian Ruble",
|
||||||
|
IsActive = true,
|
||||||
|
Symbol = "₽"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("b960166a-f7e9-49e3-bb4b-28511f126c08"),
|
||||||
|
CurrencyCode = "CNY",
|
||||||
|
CurrencyName = "Chinese Yuan (Renminbi)",
|
||||||
|
IsActive = true,
|
||||||
|
Symbol = "¥"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Master.ExpensesStatusMaster", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Master.ExpensesStatusMaster", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
@ -12,9 +12,9 @@ namespace Marco.Pms.Model.ViewModels.Expanses
|
|||||||
public PaymentModeMatserVM? PaymentMode { get; set; }
|
public PaymentModeMatserVM? PaymentMode { get; set; }
|
||||||
public BasicEmployeeVM? PaidBy { get; set; }
|
public BasicEmployeeVM? PaidBy { get; set; }
|
||||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||||
public BasicEmployeeVM? ReviewedBy { get; set; }
|
//public BasicEmployeeVM? ReviewedBy { get; set; }
|
||||||
public BasicEmployeeVM? ApprovedBy { get; set; }
|
//public BasicEmployeeVM? ApprovedBy { get; set; }
|
||||||
public BasicEmployeeVM? ProcessedBy { get; set; }
|
//public BasicEmployeeVM? ProcessedBy { get; set; }
|
||||||
public DateTime TransactionDate { get; set; }
|
public DateTime TransactionDate { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public string SupplerName { get; set; } = string.Empty;
|
public string SupplerName { get; set; } = string.Empty;
|
||||||
|
@ -181,13 +181,13 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
opt => opt.MapFrom(src => Guid.Parse(src.CreatedById)))
|
opt => opt.MapFrom(src => Guid.Parse(src.CreatedById)))
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.ReviewedById,
|
dest => dest.ReviewedById,
|
||||||
opt => opt.MapFrom(src => Guid.Parse(src.ReviewedById ?? "")))
|
opt => opt.MapFrom(src => src.ReviewedById != null ? Guid.Parse(src.ReviewedById) : Guid.Empty))
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.ApprovedById,
|
dest => dest.ApprovedById,
|
||||||
opt => opt.MapFrom(src => Guid.Parse(src.ApprovedById ?? "")))
|
opt => opt.MapFrom(src => src.ApprovedById != null ? Guid.Parse(src.ApprovedById) : Guid.Empty))
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.ProcessedById,
|
dest => dest.ProcessedById,
|
||||||
opt => opt.MapFrom(src => Guid.Parse(src.ProcessedById ?? "")))
|
opt => opt.MapFrom(src => src.ProcessedById != null ? Guid.Parse(src.ProcessedById) : Guid.Empty))
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.StatusId,
|
dest => dest.StatusId,
|
||||||
opt => opt.MapFrom(src => Guid.Parse(src.StatusId)))
|
opt => opt.MapFrom(src => Guid.Parse(src.StatusId)))
|
||||||
|
@ -34,8 +34,12 @@ namespace Marco.Pms.Services.Service
|
|||||||
private readonly CacheUpdateHelper _cache;
|
private readonly CacheUpdateHelper _cache;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
||||||
private static readonly Guid Rejected = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
|
||||||
private static readonly Guid PaidStatus = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
|
private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
|
||||||
|
private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8");
|
||||||
|
private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
||||||
|
private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27");
|
||||||
|
private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
|
||||||
private static readonly string Collection = "ExpensesModificationLog";
|
private static readonly string Collection = "ExpensesModificationLog";
|
||||||
public ExpensesService(
|
public ExpensesService(
|
||||||
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||||
@ -201,12 +205,14 @@ namespace Marco.Pms.Services.Service
|
|||||||
// 7. --- Return Final Success Response ---
|
// 7. --- Return Final Success Response ---
|
||||||
var message = $"{expenseVM.Count} expense records fetched successfully.";
|
var message = $"{expenseVM.Count} expense records fetched successfully.";
|
||||||
_logger.LogInfo(message);
|
_logger.LogInfo(message);
|
||||||
|
var defaultFilter = await GetObjectForfilter(tenantId);
|
||||||
var response = new
|
var response = new
|
||||||
{
|
{
|
||||||
Filter = expenseFilter,
|
CurrentFilter = expenseFilter,
|
||||||
CurrentPage = pageNumber,
|
CurrentPage = pageNumber,
|
||||||
TotalPages = totalPages,
|
TotalPages = totalPages,
|
||||||
TotalEntites = totalEntites,
|
TotalEntites = totalEntites,
|
||||||
|
DefaultFilter = defaultFilter,
|
||||||
Data = expenseVM,
|
Data = expenseVM,
|
||||||
};
|
};
|
||||||
return ApiResponse<object>.SuccessResponse(response, message, 200);
|
return ApiResponse<object>.SuccessResponse(response, message, 200);
|
||||||
@ -435,118 +441,124 @@ namespace Marco.Pms.Services.Service
|
|||||||
/// <returns>An ApiResponse containing the updated expense details or an error.</returns>
|
/// <returns>An ApiResponse containing the updated expense details or an error.</returns>
|
||||||
public async Task<ApiResponse<object>> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId)
|
public async Task<ApiResponse<object>> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId)
|
||||||
{
|
{
|
||||||
// --- 1. Fetch Existing Expense ---
|
// 1. Fetch Existing Expense with Related Entities (Single Query)
|
||||||
// We include all related entities needed for the final response mapping to avoid multiple database trips.
|
var expense = await _context.Expenses
|
||||||
// The query also ensures we don't process a request if the status is already the one requested.
|
|
||||||
var existingExpense = await _context.Expenses
|
|
||||||
.Include(e => e.ExpensesType)
|
.Include(e => e.ExpensesType)
|
||||||
.Include(e => e.Project)
|
.Include(e => e.Project)
|
||||||
.Include(e => e.PaidBy)
|
.Include(e => e.PaidBy).ThenInclude(e => e!.JobRole)
|
||||||
.ThenInclude(e => e!.JobRole)
|
|
||||||
.Include(e => e.PaymentMode)
|
.Include(e => e.PaymentMode)
|
||||||
.Include(e => e.Status)
|
.Include(e => e.Status)
|
||||||
.Include(e => e.CreatedBy)
|
.Include(e => e.CreatedBy)
|
||||||
.FirstOrDefaultAsync(e => e.Id == model.ExpenseId && e.StatusId != model.StatusId && e.TenantId == tenantId);
|
.Include(e => e.ReviewedBy)
|
||||||
|
.Include(e => e.ApprovedBy)
|
||||||
|
.Include(e => e.ProcessedBy)
|
||||||
|
.FirstOrDefaultAsync(e =>
|
||||||
|
e.Id == model.ExpenseId &&
|
||||||
|
e.StatusId != model.StatusId &&
|
||||||
|
e.TenantId == tenantId
|
||||||
|
);
|
||||||
|
|
||||||
if (existingExpense == null)
|
if (expense == null)
|
||||||
{
|
{
|
||||||
// Use structured logging for better searchability.
|
_logger.LogWarning("ChangeStatus: Expense not found or already at target status. ExpenseId={ExpenseId}, TenantId={TenantId}", model.ExpenseId, tenantId);
|
||||||
_logger.LogWarning("Attempted to change status for a non-existent or already-updated expense. ExpenseId: {ExpenseId}, TenantId: {TenantId}", model.ExpenseId, tenantId);
|
|
||||||
return ApiResponse<object>.ErrorResponse("Expense not found or status is already set.", "Expense not found", 404);
|
return ApiResponse<object>.ErrorResponse("Expense not found or status is already set.", "Expense not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInfo("Initiating status change for ExpenseId: {ExpenseId} from StatusId: {OldStatusId} to {NewStatusId}",
|
_logger.LogInfo("ChangeStatus: Requested status change. ExpenseId={ExpenseId} FromStatus={FromStatusId} ToStatus={ToStatusId}",
|
||||||
existingExpense.Id, existingExpense.StatusId, model.StatusId);
|
expense.Id, expense.StatusId, model.StatusId);
|
||||||
|
|
||||||
// --- 2. Concurrently Check Prerequisites ---
|
// 2. Run Prerequisite Checks in Parallel (Status transition + Permissions)
|
||||||
// We run status validation and permission fetching in parallel for efficiency.
|
var statusTransitionTask = Task.Run(async () =>
|
||||||
// Using Task.Run with an async lambda is the standard way to start a concurrent,
|
|
||||||
// CPU- or I/O-bound operation on a background thread.
|
|
||||||
|
|
||||||
// Task to validate if the requested status change is a valid transition.
|
|
||||||
var statusMappingTask = Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
// 'await using' ensures the DbContext created by the factory is properly disposed
|
|
||||||
// within the scope of this background task.
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.ExpensesStatusMapping
|
return await dbContext.ExpensesStatusMapping
|
||||||
.Include(s => s.NextStatus)
|
.Include(m => m.NextStatus)
|
||||||
.FirstOrDefaultAsync(s => s.StatusId == existingExpense.StatusId && s.NextStatusId == model.StatusId);
|
.FirstOrDefaultAsync(m => m.StatusId == expense.StatusId && m.NextStatusId == model.StatusId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Task to fetch all permissions required for the *target* status.
|
var targetStatusPermissionsTask = Task.Run(async () =>
|
||||||
var statusPermissionMappingTask = Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.StatusPermissionMapping
|
return await dbContext.StatusPermissionMapping
|
||||||
.Where(sp => sp.StatusId == model.StatusId)
|
.Where(spm => spm.StatusId == model.StatusId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Await both tasks to complete concurrently.
|
await Task.WhenAll(statusTransitionTask, targetStatusPermissionsTask);
|
||||||
await Task.WhenAll(statusMappingTask, statusPermissionMappingTask);
|
var statusTransition = await statusTransitionTask;
|
||||||
|
var requiredPermissions = await targetStatusPermissionsTask;
|
||||||
|
|
||||||
// Now you can safely get the results.
|
// 3. Validate Transition and Required Fields
|
||||||
var statusMapping = await statusMappingTask;
|
if (statusTransition == null)
|
||||||
var statusPermissions = await statusPermissionMappingTask;
|
|
||||||
|
|
||||||
// --- 3. Validate Status Transition and Permissions ---
|
|
||||||
if (statusMapping == null)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Invalid status transition attempted for ExpenseId: {ExpenseId}. From StatusId: {FromStatusId} to {ToStatusId}",
|
_logger.LogWarning("ChangeStatus: Invalid status transition. ExpenseId={ExpenseId}, FromStatus={FromStatus}, ToStatus={ToStatus}",
|
||||||
existingExpense.Id, existingExpense.StatusId, model.StatusId);
|
expense.Id, expense.StatusId, model.StatusId);
|
||||||
return ApiResponse<object>.ErrorResponse("This status change is not allowed.", "Invalid Transition", 400);
|
return ApiResponse<object>.ErrorResponse("Status change is not permitted.", "Invalid Transition", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusMapping.NextStatusId == PaidStatus &&
|
// Validate special logic for "Processed"
|
||||||
|
if (statusTransition.NextStatusId == Processed &&
|
||||||
(string.IsNullOrWhiteSpace(model.ReimburseTransactionId) ||
|
(string.IsNullOrWhiteSpace(model.ReimburseTransactionId) ||
|
||||||
!model.ReimburseDate.HasValue ||
|
!model.ReimburseDate.HasValue ||
|
||||||
model.ReimburseById == null ||
|
model.ReimburseById == null ||
|
||||||
model.ReimburseById == Guid.Empty))
|
model.ReimburseById == Guid.Empty))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Invalid status transition attempted for ExpenseId: {ExpenseId}. From StatusId: {FromStatusId} to {ToStatusId}",
|
_logger.LogWarning("ChangeStatus: Missing reimbursement fields for 'Processed'. ExpenseId={ExpenseId}", expense.Id);
|
||||||
existingExpense.Id, existingExpense.StatusId, model.StatusId);
|
return ApiResponse<object>.ErrorResponse("Reimbursement details are missing or invalid.", "Invalid Reimbursement", 400);
|
||||||
return ApiResponse<object>.ErrorResponse("This status change is not allowed.", "Invalid Transition", 400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permissions. The logic is:
|
// 4. Permission Check (CreatedBy -> Reviewer bypass, else required permissions)
|
||||||
// 1. If the target status has specific permissions defined, the user must have at least one of them.
|
|
||||||
// 2. If no permissions are defined for the target status, only the original creator of the expense can change it.
|
|
||||||
bool hasPermission = false;
|
bool hasPermission = false;
|
||||||
if (statusPermissions.Any())
|
if (model.StatusId == Review && expense.CreatedById == loggedInEmployee.Id)
|
||||||
|
{
|
||||||
|
hasPermission = true;
|
||||||
|
}
|
||||||
|
else if (requiredPermissions.Any())
|
||||||
{
|
{
|
||||||
// Using a scope to resolve scoped services like PermissionServices.
|
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
foreach (var sp in statusPermissions)
|
foreach (var permission in requiredPermissions)
|
||||||
{
|
{
|
||||||
if (await permissionService.HasPermission(sp.PermissionId, loggedInEmployee.Id))
|
if (await permissionService.HasPermission(permission.PermissionId, loggedInEmployee.Id))
|
||||||
{
|
{
|
||||||
hasPermission = true;
|
hasPermission = true;
|
||||||
break; // User has one of the required permissions, no need to check further.
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (existingExpense.CreatedById == loggedInEmployee.Id)
|
|
||||||
{
|
|
||||||
// Fallback: If no permissions are required for the status, allow the creator to make the change.
|
|
||||||
hasPermission = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPermission)
|
if (!hasPermission)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Access DENIED for EmployeeId: {EmployeeId} attempting to change status of ExpenseId: {ExpenseId} to StatusId: {NewStatusId}",
|
_logger.LogWarning("ChangeStatus: Permission denied. EmployeeId={EmployeeId} ExpenseId={ExpenseId} ToStatus={ToStatusId}",
|
||||||
loggedInEmployee.Id, existingExpense.Id, model.StatusId);
|
loggedInEmployee.Id, expense.Id, model.StatusId);
|
||||||
return ApiResponse<object>.ErrorResponse("You do not have the required permissions to perform this action.", "Access Denied", 403);
|
return ApiResponse<object>.ErrorResponse("You do not have permission for this action.", "Access Denied", 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. Update Expense and Add Log (in a transaction) ---
|
// 5. Prepare for update (Audit snapshot)
|
||||||
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(existingExpense); // Capture state for audit log BEFORE changes.
|
var expenseStateBeforeChange = _updateLogHelper.EntityToBsonDocument(expense);
|
||||||
|
|
||||||
existingExpense.StatusId = statusMapping.NextStatusId;
|
// 6. Apply Status Transition
|
||||||
existingExpense.Status = statusMapping.NextStatus; // Assigning the included entity for the response mapping.
|
expense.StatusId = statusTransition.NextStatusId;
|
||||||
|
expense.Status = statusTransition.NextStatus;
|
||||||
|
|
||||||
var expensesRemburse = new ExpensesReimburse
|
// Handle reviewer/approver/processor fields based on target StatusId (Guid)
|
||||||
|
if (model.StatusId == Approve || model.StatusId == RejectedByReviewer)
|
||||||
|
{
|
||||||
|
expense.ReviewedById = loggedInEmployee.Id;
|
||||||
|
}
|
||||||
|
else if (model.StatusId == ProcessPending || model.StatusId == RejectedByApprover)
|
||||||
|
{
|
||||||
|
expense.ApprovedById = loggedInEmployee.Id;
|
||||||
|
}
|
||||||
|
else if (model.StatusId == Processed)
|
||||||
|
{
|
||||||
|
expense.ProcessedById = loggedInEmployee.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Add Reimbursement if applicable
|
||||||
|
if (model.StatusId == Processed)
|
||||||
|
{
|
||||||
|
var reimbursement = new ExpensesReimburse
|
||||||
{
|
{
|
||||||
ReimburseTransactionId = model.ReimburseTransactionId!,
|
ReimburseTransactionId = model.ReimburseTransactionId!,
|
||||||
ReimburseDate = model.ReimburseDate!.Value,
|
ReimburseDate = model.ReimburseDate!.Value,
|
||||||
@ -554,92 +566,77 @@ namespace Marco.Pms.Services.Service
|
|||||||
ReimburseNote = model.Comment ?? string.Empty,
|
ReimburseNote = model.Comment ?? string.Empty,
|
||||||
TenantId = tenantId
|
TenantId = tenantId
|
||||||
};
|
};
|
||||||
_context.ExpensesReimburse.Add(expensesRemburse);
|
_context.ExpensesReimburse.Add(reimbursement);
|
||||||
|
|
||||||
_context.ExpensesReimburseMapping.Add(new ExpensesReimburseMapping
|
_context.ExpensesReimburseMapping.Add(new ExpensesReimburseMapping
|
||||||
{
|
{
|
||||||
ExpensesId = existingExpense.Id,
|
ExpensesId = expense.Id,
|
||||||
ExpensesReimburseId = expensesRemburse.Id,
|
ExpensesReimburseId = reimbursement.Id,
|
||||||
TenantId = tenantId
|
TenantId = tenantId
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Add Expense Log Entry
|
||||||
_context.ExpenseLogs.Add(new ExpenseLog
|
_context.ExpenseLogs.Add(new ExpenseLog
|
||||||
{
|
{
|
||||||
ExpenseId = existingExpense.Id,
|
ExpenseId = expense.Id,
|
||||||
Action = $"Status changed to '{statusMapping.NextStatus!.Name}'",
|
Action = $"Status changed to '{statusTransition.NextStatus?.Name}'",
|
||||||
UpdatedById = loggedInEmployee.Id,
|
UpdatedById = loggedInEmployee.Id,
|
||||||
Comment = model.Comment,
|
Comment = model.Comment,
|
||||||
TenantId = tenantId
|
TenantId = tenantId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 9. Commit database transaction
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
_logger.LogInfo("Successfully updated status for ExpenseId: {ExpenseId} to StatusId: {NewStatusId}", existingExpense.Id, existingExpense.StatusId);
|
_logger.LogInfo("ChangeStatus: Status updated successfully. ExpenseId={ExpenseId} NewStatus={NewStatusId}", expense.Id, expense.StatusId);
|
||||||
}
|
}
|
||||||
catch (DbUpdateConcurrencyException dbEx)
|
catch (DbUpdateConcurrencyException ex)
|
||||||
{
|
{
|
||||||
// This error occurs if the record was modified by someone else after we fetched it.
|
_logger.LogError(ex, "ChangeStatus: Concurrency error. ExpenseId={ExpenseId}", expense.Id);
|
||||||
_logger.LogError(dbEx, "Concurrency conflict while updating status for ExpenseId: {ExpenseId}. The record may have been modified by another user.", existingExpense.Id);
|
return ApiResponse<object>.ErrorResponse("Expense was modified by another user. Please refresh and try again.", "Concurrency Error", 409);
|
||||||
return ApiResponse<object>.ErrorResponse("The expense was modified by another user. Please refresh and try again.", "Concurrency Error", 409); // 409 Conflict is appropriate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 5. Perform Post-Save Actions (Audit Log and Fetching Next State for UI) ---
|
// 10. Post-processing (audit log, cache, fetch next states)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Task to save the detailed audit log to a separate system (e.g., MongoDB).
|
var auditLogTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||||
var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
|
||||||
{
|
{
|
||||||
EntityId = existingExpense.Id.ToString(),
|
EntityId = expense.Id.ToString(),
|
||||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||||
OldObject = existingEntityBson,
|
OldObject = expenseStateBeforeChange,
|
||||||
UpdatedAt = DateTime.UtcNow
|
UpdatedAt = DateTime.UtcNow
|
||||||
}, Collection);
|
}, Collection);
|
||||||
|
|
||||||
var cacheUpdateTask = _cache.ReplaceExpenseAsync(existingExpense);
|
var cacheUpdateTask = _cache.ReplaceExpenseAsync(expense);
|
||||||
|
|
||||||
// Task to get all possible next statuses from the *new* current state to help the UI.
|
var getNextStatusesTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(async t =>
|
||||||
// NOTE: This now fetches a list of all possible next states, which is more useful for a UI.
|
|
||||||
var getNextStatusesTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(t =>
|
|
||||||
{
|
{
|
||||||
var dbContext = t.Result;
|
var dbContext = t.Result;
|
||||||
return dbContext.ExpensesStatusMapping
|
var nextStatuses = await dbContext.ExpensesStatusMapping
|
||||||
.Include(s => s.NextStatus)
|
.Include(m => m.NextStatus)
|
||||||
.Where(s => s.StatusId == existingExpense.StatusId && s.NextStatus != null)
|
.Where(m => m.StatusId == expense.StatusId && m.NextStatus != null)
|
||||||
.Select(s => s.NextStatus) // Select only the status object
|
.Select(m => m.NextStatus)
|
||||||
.ToListAsync()
|
.ToListAsync();
|
||||||
.ContinueWith(res =>
|
await dbContext.DisposeAsync();
|
||||||
{
|
return nextStatuses;
|
||||||
dbContext.Dispose(); // Ensure the context is disposed
|
|
||||||
return res.Result;
|
|
||||||
});
|
|
||||||
}).Unwrap();
|
}).Unwrap();
|
||||||
|
|
||||||
await Task.WhenAll(mongoDBTask, getNextStatusesTask, cacheUpdateTask);
|
await Task.WhenAll(auditLogTask, getNextStatusesTask, cacheUpdateTask);
|
||||||
|
|
||||||
|
// Prepare response with possible next states
|
||||||
var nextPossibleStatuses = await getNextStatusesTask;
|
var nextPossibleStatuses = await getNextStatusesTask;
|
||||||
|
var responseDto = _mapper.Map<ExpenseList>(expense);
|
||||||
|
if (nextPossibleStatuses is { Count: > 0 })
|
||||||
|
responseDto.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(nextPossibleStatuses);
|
||||||
|
|
||||||
var response = _mapper.Map<ExpenseList>(existingExpense);
|
return ApiResponse<object>.SuccessResponse(responseDto);
|
||||||
if (nextPossibleStatuses != null)
|
|
||||||
{
|
|
||||||
// The response DTO should have a property like: public List<ExpensesStatusMasterVM> NextAvailableStatuses { get; set; }
|
|
||||||
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(nextPossibleStatuses);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiResponse<object>.SuccessResponse(response);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// This catch block handles errors from post-save operations like MongoDB logging.
|
_logger.LogError(ex, "ChangeStatus: Post-operation error (e.g. audit logging). ExpenseId={ExpenseId}", expense.Id);
|
||||||
// The primary update was successful, but we must log this failure.
|
var responseDto = _mapper.Map<ExpenseList>(expense);
|
||||||
_logger.LogError(ex, "Error occurred during post-save operations for ExpenseId: {ExpenseId} (e.g., audit logging). The primary status change was successful.", existingExpense.Id);
|
return ApiResponse<object>.SuccessResponse(responseDto, "Status updated, but audit logging or cache update failed.");
|
||||||
|
|
||||||
// We can still return a success response because the main operation succeeded,
|
|
||||||
// but we should not block the user for a failed audit log.
|
|
||||||
// Alternatively, if audit logging is critical, you could return an error.
|
|
||||||
// Here, we choose to return success but log the ancillary failure.
|
|
||||||
var response = _mapper.Map<ExpenseList>(existingExpense);
|
|
||||||
return ApiResponse<object>.SuccessResponse(response, "Status updated, but a post-processing error occurred.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,6 +659,9 @@ namespace Marco.Pms.Services.Service
|
|||||||
.Include(e => e.PaymentMode)
|
.Include(e => e.PaymentMode)
|
||||||
.Include(e => e.Status)
|
.Include(e => e.Status)
|
||||||
.Include(e => e.CreatedBy)
|
.Include(e => e.CreatedBy)
|
||||||
|
.Include(e => e.ReviewedBy)
|
||||||
|
.Include(e => e.ApprovedBy)
|
||||||
|
.Include(e => e.ProcessedBy)
|
||||||
.FirstOrDefaultAsync(e =>
|
.FirstOrDefaultAsync(e =>
|
||||||
e.Id == model.Id &&
|
e.Id == model.Id &&
|
||||||
e.TenantId == tenantId);
|
e.TenantId == tenantId);
|
||||||
@ -673,7 +673,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404);
|
return ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingExpense.StatusId != Draft && existingExpense.StatusId != Rejected)
|
if (existingExpense.StatusId != Draft && existingExpense.StatusId != RejectedByReviewer && existingExpense.StatusId != RejectedByApprover)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User attempted to update expense with ID {ExpenseId}, but donot have status of DRAFT or REJECTED", loggedInEmployee.Id);
|
_logger.LogWarning("User attempted to update expense with ID {ExpenseId}, but donot have status of DRAFT or REJECTED", loggedInEmployee.Id);
|
||||||
return ApiResponse<object>.ErrorResponse("Expense connot be updated", "Expense connot be updated", 400);
|
return ApiResponse<object>.ErrorResponse("Expense connot be updated", "Expense connot be updated", 400);
|
||||||
@ -1040,6 +1040,10 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
private async Task<ExpenseDetailsVM> GetAllExpnesRelatedTablesFromMongoDB(ExpenseDetailsMongoDB model, Guid tenantId)
|
private async Task<ExpenseDetailsVM> GetAllExpnesRelatedTablesFromMongoDB(ExpenseDetailsMongoDB model, Guid tenantId)
|
||||||
{
|
{
|
||||||
|
var reviewedById = model.ReviewedById != null ? Guid.Parse(model.ReviewedById) : Guid.Empty;
|
||||||
|
var approvedById = model.ApprovedById != null ? Guid.Parse(model.ApprovedById) : Guid.Empty;
|
||||||
|
var processedById = model.ProcessedById != null ? Guid.Parse(model.ProcessedById) : Guid.Empty;
|
||||||
|
|
||||||
var projectTask = Task.Run(async () =>
|
var projectTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
@ -1048,12 +1052,27 @@ namespace Marco.Pms.Services.Service
|
|||||||
var paidByTask = Task.Run(async () =>
|
var paidByTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Employees.AsNoTracking().FirstOrDefaultAsync(e => e.Id == Guid.Parse(model.PaidById) && e.TenantId == tenantId);
|
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == Guid.Parse(model.PaidById) && e.TenantId == tenantId);
|
||||||
});
|
});
|
||||||
var createdByTask = Task.Run(async () =>
|
var createdByTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Employees.AsNoTracking().FirstOrDefaultAsync(e => e.Id == Guid.Parse(model.CreatedById) && e.TenantId == tenantId);
|
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == Guid.Parse(model.CreatedById) && e.TenantId == tenantId);
|
||||||
|
});
|
||||||
|
var reviewedByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == reviewedById && e.TenantId == tenantId);
|
||||||
|
});
|
||||||
|
var approvedByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == approvedById && e.TenantId == tenantId);
|
||||||
|
});
|
||||||
|
var processedByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == processedById && e.TenantId == tenantId);
|
||||||
});
|
});
|
||||||
var expenseTypeTask = Task.Run(async () =>
|
var expenseTypeTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@ -1103,12 +1122,15 @@ namespace Marco.Pms.Services.Service
|
|||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.ExpensesReimburseMapping
|
return await dbContext.ExpensesReimburseMapping
|
||||||
.Include(er => er.ExpensesReimburse)
|
.Include(er => er.ExpensesReimburse)
|
||||||
|
.ThenInclude(er => er!.ReimburseBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
.Where(er => er.TenantId == tenantId && er.ExpensesId == Guid.Parse(model.Id))
|
.Where(er => er.TenantId == tenantId && er.ExpensesId == Guid.Parse(model.Id))
|
||||||
.Select(er => er.ExpensesReimburse).FirstOrDefaultAsync();
|
.Select(er => er.ExpensesReimburse).FirstOrDefaultAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Await all prerequisite checks at once.
|
// Await all prerequisite checks at once.
|
||||||
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, statusTask, permissionStatusMappingTask, expenseReimburseTask);
|
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask, processedByTask,
|
||||||
|
statusTask, permissionStatusMappingTask, expenseReimburseTask);
|
||||||
|
|
||||||
var project = projectTask.Result;
|
var project = projectTask.Result;
|
||||||
var expenseType = expenseTypeTask.Result;
|
var expenseType = expenseTypeTask.Result;
|
||||||
@ -1117,6 +1139,9 @@ namespace Marco.Pms.Services.Service
|
|||||||
var permissionStatusMappings = permissionStatusMappingTask.Result;
|
var permissionStatusMappings = permissionStatusMappingTask.Result;
|
||||||
var paidBy = paidByTask.Result;
|
var paidBy = paidByTask.Result;
|
||||||
var createdBy = createdByTask.Result;
|
var createdBy = createdByTask.Result;
|
||||||
|
var reviewedBy = reviewedByTask.Result;
|
||||||
|
var approvedBy = approvedByTask.Result;
|
||||||
|
var processedBy = processedByTask.Result;
|
||||||
var expensesReimburse = expenseReimburseTask.Result;
|
var expensesReimburse = expenseReimburseTask.Result;
|
||||||
|
|
||||||
var response = _mapper.Map<ExpenseDetailsVM>(model);
|
var response = _mapper.Map<ExpenseDetailsVM>(model);
|
||||||
@ -1124,6 +1149,9 @@ namespace Marco.Pms.Services.Service
|
|||||||
response.Project = _mapper.Map<ProjectInfoVM>(project);
|
response.Project = _mapper.Map<ProjectInfoVM>(project);
|
||||||
response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
|
response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
|
||||||
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdBy);
|
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdBy);
|
||||||
|
if (reviewedBy != null) response.ReviewedBy = _mapper.Map<BasicEmployeeVM>(reviewedBy);
|
||||||
|
if (approvedBy != null) response.ApprovedBy = _mapper.Map<BasicEmployeeVM>(approvedBy);
|
||||||
|
if (processedBy != null) response.ProcessedBy = _mapper.Map<BasicEmployeeVM>(processedBy);
|
||||||
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
|
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
|
||||||
response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);
|
response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);
|
||||||
response.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
|
response.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
|
||||||
@ -1158,6 +1186,69 @@ namespace Marco.Pms.Services.Service
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<object> GetObjectForfilter(Guid tenantId)
|
||||||
|
{
|
||||||
|
// Task 1: Get all distinct projects associated with the tenant's expenses.
|
||||||
|
var projectsTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Expenses
|
||||||
|
.Where(e => e.TenantId == tenantId && e.Project != null)
|
||||||
|
.Select(e => e.Project!)
|
||||||
|
.Distinct()
|
||||||
|
.Select(p => new { p.Id, Name = $"{p.Name}" })
|
||||||
|
.ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task 2: Get all distinct users who paid for the tenant's expenses.
|
||||||
|
var paidByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Expenses
|
||||||
|
.Where(e => e.TenantId == tenantId && e.PaidBy != null)
|
||||||
|
.Select(e => e.PaidBy!)
|
||||||
|
.Distinct()
|
||||||
|
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
|
||||||
|
.ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task 3: Get all distinct users who created the tenant's expenses.
|
||||||
|
var createdByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Expenses
|
||||||
|
.Where(e => e.TenantId == tenantId && e.CreatedBy != null)
|
||||||
|
.Select(e => e.CreatedBy!)
|
||||||
|
.Distinct()
|
||||||
|
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
|
||||||
|
.ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task 4: Get all distinct statuses associated with the tenant's expenses.
|
||||||
|
var statusTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Expenses
|
||||||
|
.Where(e => e.TenantId == tenantId && e.Status != null)
|
||||||
|
.Select(e => e.Status!)
|
||||||
|
.Distinct()
|
||||||
|
.Select(s => new { s.Id, s.Name })
|
||||||
|
.ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute all four queries concurrently. The total wait time will be determined
|
||||||
|
// by the longest-running query, not the sum of all four.
|
||||||
|
await Task.WhenAll(projectsTask, paidByTask, createdByTask, statusTask);
|
||||||
|
|
||||||
|
// Construct the final object from the results of the completed tasks.
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Projects = await projectsTask,
|
||||||
|
PaidBy = await paidByTask,
|
||||||
|
CreatedBy = await createdByTask,
|
||||||
|
Status = await statusTask
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
||||||
|
Loading…
x
Reference in New Issue
Block a user