LSFE/LSFE.Domain/Services/Planning/PlanningRepo.cs
2026-07-30 10:31:04 +08:00

668 lines
25 KiB
C#

using LSFE.Domain.Contracts.Planning;
using LSFE.Infrastructure.Database;
using LSFE.Infrastructure.Dto;
using LSFE.Infrastructure.Dto.Doctor;
using LSFE.Infrastructure.Dto.Planning;
using LSFE.Infrastructure.Entities.Maintenance;
using LSFE.Infrastructure.Entities.Planning;
using LSFE.Infrastructure.Model;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualBasic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using static LSFE.Domain.Services.OutputParamMessage;
namespace LSFE.Domain.Services.Planning
{
public class PlanningRepo : GenericRepository<MRRawPlan>, IPlanningRepo
{
private readonly LSFEDbContext _context;
public PlanningRepo(LSFEDbContext context) : base(context)
{
_context = context;
}
#region Post Put
public async Task<Response> SoftDeleteAsync(int doctorId)
{
var response = new Response();
try
{
var existingDoctor = await GetByIdAsync(doctorId);
if (existingDoctor == null)
{
response.Success = false;
response.Message = $"Doctor with ID {doctorId} not found.";
response.MessCode = 0;
return response;
}
Update(existingDoctor);
await _context.SaveChangesAsync();
response.Success = true;
response.Data = existingDoctor;
response.Message = "Doctor deactivated successfully.";
response.MessCode = 3; // code for delete/soft delete
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
response.MessCode = 0;
}
return response;
}
public async Task<Response> PostPutPlan(PlanningDto dto)
{
// Input validation
if (dto == null)
return new Response { Success = false, Message = "Invalid request data", MessCode = 0 };
var (messCode, message, planId) = CreateOutputParamsPlanId();
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Execute the stored procedure
await _context.Database.ExecuteSqlRawAsync(
"EXEC PostPutPlan @UserId,@DoctorId,@CallDate,@CallType,@SLP," +
"@IsJoinCall,@IsLiterature,@MRPlanDetailId,@MRRawPlanId,@IsUpdateDelete,@MessCode OUTPUT,@Message OUTPUT,@MRRawPlanIdOutPut OUTPUT",
new SqlParameter("@UserId", dto.UserId),
new SqlParameter("@DoctorId", dto.DoctorId),
new SqlParameter("@CallDate", dto.CallDate),
new SqlParameter("@CallType", dto.SLP ?? "N/A"),
new SqlParameter("@SLP", dto.SLP ?? "N/A"),
new SqlParameter("@IsJoinCall", false),
new SqlParameter("@IsLiterature", dto.IsLiterature),
new SqlParameter("@MRPlanDetailId",
string.IsNullOrEmpty(dto.MRPlanDetailId) || dto.MRPlanDetailId == "N/A"
? (object)DBNull.Value
: Guid.Parse(dto.MRPlanDetailId)),
new SqlParameter("@MRRawPlanId",
string.IsNullOrEmpty(dto.MRRawPlanId) || dto.MRRawPlanId == "N/A"
? (object)DBNull.Value
: Guid.Parse(dto.MRRawPlanId)),
new SqlParameter("@IsUpdateDelete", dto.IsUpdateDelete),
messCode,
message,
planId);
var code = (byte)(messCode.Value ?? 0);
var msg = message.Value?.ToString() ?? "No response message.";
var mrRawPlanId = planId.Value?.ToString() ?? string.Empty;
var success = code != 0;
// If stored procedure failed, rollback and return
if (!success)
{
await transaction.RollbackAsync();
return new Response { Success = false, Message = msg, MessCode = code };
}
// Validate that we have a valid plan ID for insert/update operations
if (string.IsNullOrEmpty(mrRawPlanId) && dto.IsUpdateDelete != 1)
{
await transaction.RollbackAsync();
return new Response { Success = false, Message = "You cannot create more than one visit in a week!", MessCode = 0 };
}
// Get username for audit trail
var username = await _context.Users
.Where(u => u.Id == dto.UserId)
.Select(u => u.UserName)
.FirstOrDefaultAsync();
if (string.IsNullOrEmpty(username))
{
await transaction.RollbackAsync();
return new Response { Success = false, Message = "User not found", MessCode = 0 };
}
var now = DateTime.Now;
DateOnly today = DateOnly.FromDateTime(DateTime.Now);
// Handle inventory based on operation type
if (dto.IsUpdateDelete == 1 || dto.IsUpdateDelete == 2) // UPDATE/DELETE
{
if (!IsSameMonth(dto.CallDate, today))
{
msg = await HandleInventoryDelete(dto, msg);
msg = await HandlePromosDelete(dto, msg);
msg = await HandleInventoryInsert(dto, msg, username, now, mrRawPlanId);
msg = await HandlePromosInsert(dto, msg, username, now, mrRawPlanId);
}
}
else if (dto.IsUpdateDelete == 2) // UPDATE
{
if (!IsSameMonth(dto.CallDate, today))
{
msg = await HandleInventoryUpdate(dto, msg, username, now);
msg = await HandlePromosUpdate(dto, msg, username, now, mrRawPlanId);
}
}
else // INSERT (new plan)
{
msg = await HandleInventoryInsert(dto, msg, username, now, mrRawPlanId);
msg = await HandlePromosInsert(dto, msg, username, now, mrRawPlanId);
}
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return new Response
{
Success = true,
Message = msg,
MessCode = code
};
}
catch (Exception ex)
{
await transaction.RollbackAsync();
return new Response
{
Success = false,
Message = $"Error processing plan: {ex.Message}",
MessCode = 0
};
}
}
private bool IsSameMonth(DateOnly date, DateOnly reference)
{
return date.Year == reference.Year && date.Month == reference.Month;
}
private async Task<string> HandleInventoryDelete(PlanningDto dto, string msg)
{
if (string.IsNullOrEmpty(dto.MRRawPlanId))
return msg + " Cannot delete inventory: Missing MRRawPlanId.";
var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId);
// Get all inventory transactions (ONE query only)
var transactions = await _context.InventoryTransactions
.Where(t => t.MRRawPlanId == mrRawPlanGuid)
.ToListAsync();
if (transactions.Count == 0)
return msg + " No linked inventory transactions found.";
// Collect inventory IDs for batch query
var inventoryIds = transactions
.Select(t => t.InventoryId)
.Distinct()
.ToList();
// Load inventories in ONE query
var inventories = await _context.Inventory
.Where(i => inventoryIds.Contains(i.InventoryId))
.ToDictionaryAsync(i => i.InventoryId);
// Process each transaction update
foreach (var tx in transactions)
{
if (!inventories.TryGetValue(tx.InventoryId, out var inventory))
continue; // if missing, skip safely
inventory.QtyOut -= tx.QtyIn; // revert qty out
inventory.QtyBalance += tx.QtyIn; // restore qty
if (inventory.QtyOut < 0)
inventory.QtyOut = 0; // safety check
_context.Inventory.Update(inventory);
}
// Remove all transaction records
_context.InventoryTransactions.RemoveRange(transactions);
return msg + $" Deleted {transactions.Count} record(s).";
}
private async Task<string> HandleInventoryUpdate(PlanningDto dto, string msg,
string username, DateTime now)
{
if (dto.SelectedSamples == null || !dto.SelectedSamples.Any())
return msg;
if (string.IsNullOrEmpty(dto.MRRawPlanId))
{
return msg + " Cannot update inventory: Missing MRRawPlanId.";
}
var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId);
foreach (var sample in dto.SelectedSamples)
{
var inventoryTransaction = await _context.InventoryTransactions
.FirstOrDefaultAsync(i => i.InventoryId == sample.InventoryId
&& i.MRRawPlanId == mrRawPlanGuid);
var inventory = await _context.Inventory
.FirstOrDefaultAsync(i => i.InventoryId == sample.InventoryId);
if (inventory == null)
{
msg += $" {sample.InventoryId}: Inventory not found.";
continue;
}
if (inventoryTransaction == null)
{
msg += $" {inventory.Description}: No existing transaction found.";
continue;
}
// Calculate quantity difference
int newQtyBalance = inventory.QtyIn - sample.SelectedQty;
//Update the new qty balance
inventory.QtyBalance = newQtyBalance;
// Validate balance
if (inventory.QtyBalance < 0)
{
msg += $" {inventory.Description}: Insufficient stock for update.";
continue;
}
if (inventory.QtyBalance > inventory.QtyIn)
{
msg += $" {inventory.Description}: Balance exceeds original quantity.";
continue;
}
// Update transaction
inventoryTransaction.AppointmentDate = dto.CallDate;
inventoryTransaction.QtyOut = sample.SelectedQty;
inventoryTransaction.QtyIn = sample.SelectedQty;
inventoryTransaction.UpdatedBy = username;
inventoryTransaction.UpdatedDate = now;
_context.InventoryTransactions.Update(inventoryTransaction);
_context.Inventory.Update(inventory);
msg += $" {inventory.Description}: Updated from {sample.OriginalQty} to {sample.SelectedQty} units.";
}
return msg;
}
private async Task<string> HandleInventoryInsert(PlanningDto dto, string msg,
string username, DateTime now, string mrRawPlanId)
{
if (dto.SelectedSamples == null || !dto.SelectedSamples.Any())
return msg;
if (!Guid.TryParse(mrRawPlanId, out var planGuid))
{
return msg + " Invalid plan ID for inventory insert.";
}
var inventoryWarnings = new List<string>();
foreach (var sample in dto.SelectedSamples)
{
var inventory = await _context.Inventory
.FirstOrDefaultAsync(i => i.InventoryId == sample.InventoryId);
if (inventory == null)
{
inventoryWarnings.Add($"{sample.InventoryId}: Not found");
continue;
}
// Calculate quantity difference
int newQtyBalance = inventory.QtyIn - sample.SelectedQty;
// Create transaction
var inventoryTransaction = new InventoryTransaction
{
InventoryId = sample.InventoryId,
DoctorId = dto.DoctorId,
Description = sample.Description ?? inventory.Description ?? "Sample",
MRRawPlanId = planGuid,
AppointmentDate = dto.CallDate,
CreatedDate = now,
QtyIn = sample.SelectedQty,
QtyOut = sample.SelectedQty,
UserId = dto.UserId,
CreatedBy = username,
IsActive = true
};
_context.InventoryTransactions.Add(inventoryTransaction);
// Deduct from inventory
inventory.QtyBalance = newQtyBalance;
inventory.QtyOut = sample.SelectedQty;
inventory.UpdatedBy = username;
inventory.UpdatedDate = now;
_context.Inventory.Update(inventory);
}
if (inventoryWarnings.Any())
{
msg += $" (Inventory warnings: {string.Join("; ", inventoryWarnings)})";
}
return msg;
}
private async Task<string> HandlePromosDelete(PlanningDto dto, string msg)
{
if (dto.SelectedPromos == null || !dto.SelectedPromos.Any())
return msg;
if (string.IsNullOrEmpty(dto.MRRawPlanId))
{
return msg + " Cannot delete promos: Missing MRRawPlanId.";
}
var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId);
var promosToDelete = await _context.ProductTransactions
.Where(pt => pt.MRRawPlanId == mrRawPlanGuid)
.ToListAsync();
_context.ProductTransactions.RemoveRange(promosToDelete);
return msg + $" Deleted {promosToDelete.Count} promo(s).";
}
private async Task<string> HandlePromosUpdate(PlanningDto dto, string msg,
string username, DateTime now, string mrRawPlanId)
{
if (dto.SelectedPromos == null || !dto.SelectedPromos.Any())
return msg;
if (string.IsNullOrEmpty(dto.MRRawPlanId))
{
return msg + " Cannot update promos: Missing MRRawPlanId.";
}
var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId);
// Delete existing promos
var existingPromos = await _context.ProductTransactions
.Where(pt => pt.MRRawPlanId == mrRawPlanGuid)
.ToListAsync();
_context.ProductTransactions.RemoveRange(existingPromos);
// Insert new promos
foreach (var product in dto.SelectedPromos)
{
var productTransaction = new ProductTransaction
{
ProductId = product.ProductId,
DoctorId = dto.DoctorId,
ProductName = product.ProductName,
AppointmentDate = dto.CallDate,
MRRawPlanId = mrRawPlanGuid,
CreatedDate = now,
CreatedBy = username,
UserId = dto.UserId,
IsActive = true
};
_context.ProductTransactions.Add(productTransaction);
}
return msg + $" Updated promos: removed {existingPromos.Count}, added {dto.SelectedPromos.Count}.";
}
private async Task<string> HandlePromosInsert(PlanningDto dto, string msg,
string username, DateTime now, string mrRawPlanId)
{
if (dto.SelectedPromos == null || !dto.SelectedPromos.Any())
return msg;
if (!Guid.TryParse(mrRawPlanId, out var planGuid))
{
return msg + " Invalid plan ID for promo insert.";
}
foreach (var product in dto.SelectedPromos)
{
var productTransaction = new ProductTransaction
{
ProductId = product.ProductId,
DoctorId = dto.DoctorId,
ProductName= product.ProductName,
AppointmentDate = dto.CallDate,
MRRawPlanId = planGuid,
CreatedDate = now,
CreatedBy = username,
UserId = dto.UserId,
IsActive = true
};
_context.ProductTransactions.Add(productTransaction);
}
return msg;
}
public async Task<Response> ApprovePlan(ApprovePlanDto dto)
{
if (dto == null || dto.AppointmentIds == null || !dto.AppointmentIds.Any())
return new Response { Success = false, Message = "No appointments selected", MessCode = 0 };
// Filter out empty GUIDs
var guidList = dto.AppointmentIds.Where(id => id != Guid.Empty).ToList();
if (!guidList.Any())
return new Response { Success = false, Message = "No valid appointment ids", MessCode = 0 };
using var tx = await _context.Database.BeginTransactionAsync();
try
{
var plans = await _context.MRRawPlans
.Where(p => guidList.Contains(p.MRRawPlanId))
.ToListAsync();
if (!plans.Any())
return new Response { Success = false, Message = "No matching appointments found", MessCode = 0 };
int approvedCount = 0;
int alreadyApproved = 0;
var now = DateTime.UtcNow;
var userIdOrSystem = dto.UpdatedBy ?? "System";
foreach (var plan in plans)
{
if (plan.IsApproved == true)
{
alreadyApproved++;
continue;
}
plan.IsApproved = true;
plan.ApprovedBy = userIdOrSystem;
plan.ApprovedDate = now;
approvedCount++;
}
if (approvedCount > 0)
{
_context.MRRawPlans.UpdateRange(plans.Where(p => p.IsApproved == true));
await _context.SaveChangesAsync();
}
await tx.CommitAsync();
return new Response
{
Success = true,
Message = $"{approvedCount} approved, {alreadyApproved} already approved.",
MessCode = 1
};
}
catch (Exception ex)
{
await tx.RollbackAsync();
return new Response { Success = false, Message = $"Error: {ex.Message}", MessCode = 0 };
}
}
public async Task<Response> CopyMonthlyMRRawPlans(PlanningDto dto)
{
try
{
var (messCode, message) = CreateOutputParams();
// Execute the stored procedure
await _context.Database.ExecuteSqlRawAsync(
"EXEC CopyMonthlyMRRawPlans @MonthPlanCopy,@NewMonthPlan,@UserId,@MessCode OUTPUT,@Message OUTPUT",
new SqlParameter("@MonthPlanCopy", dto.MonthPlanCopy),
new SqlParameter("@NewMonthPlan", dto.NewMonthPlan),
new SqlParameter("@UserId", dto.UserId),
messCode,
message);
return new Response
{
Success = true,
Message = message.Value.ToString(),
MessCode = (byte)(messCode.Value ?? 0)
};
}
catch (Exception ex)
{
return new Response
{
Success = true,
Message = ex.ToString() ?? ex.InnerException.ToString(),
MessCode = 0
};
}
}
#endregion
#region Get
public async Task<List<DoctorWithPlan>> GetDoctorWithPlan(DoctorDto dto)
{
string spName = dto.IsPlanning
? "GetDoctorWithPlan"
: "GetDoctorWithPlanStatus";
var allItems = await _context.DoctorWithPlan
.FromSqlRaw(
$"EXEC {spName} @UserId, @MRPlanDate",
new SqlParameter("@UserId", dto.UserId),
new SqlParameter("@MRPlanDate", dto.MRPlanDate))
.ToListAsync();
return allItems ?? new List<DoctorWithPlan>();
}
public async Task<List<MRRawPlan>> GetPlan(PlanningDto dto)
{
var allItems = await _context.MRRawPlans
.FromSqlRaw("EXEC GetPlan @UserId",
new SqlParameter("@UserId", dto.UserId))
.ToListAsync();
return allItems ?? new List<MRRawPlan>();
}
public async Task<List<DoctorById>> GetDoctorById(DoctorDto dto)
{
var allItems = await _context.DoctorByIds
.FromSqlRaw("EXEC GetDoctorById @UserId,@DoctorId,@MRPlanDate",
new SqlParameter("@UserId", dto.UserId),
new SqlParameter("@DoctorId", dto.DoctorId),
new SqlParameter("@MRPlanDate", dto.MRPlanDate))
.ToListAsync();
return allItems ?? new List<DoctorById>();
}
public async Task<List<PendingApproval>> GetPendingApprovals(PlanningDto dto)
{
var mrPlanDate = GetValidDate(dto.MRPlanDate);
var allItems = await _context.PendingApprovals
.FromSqlRaw(
"EXEC GetPendingApprovals @UserId,@DoctorId,@MRPlanDate",
new SqlParameter("@UserId", dto.UserId),
new SqlParameter("@DoctorId", dto.DoctorId),
new SqlParameter("@MRPlanDate", mrPlanDate)
)
.ToListAsync();
return allItems ?? new List<PendingApproval>();
}
private static DateTime GetValidDate(object? input)
{
if (input is DateTime dt)
return dt;
if (input is string s && DateTime.TryParse(s, out var parsed))
return parsed;
return DateTime.UtcNow;
}
public async Task<List<MRRawPlan>> GetAllAppointmentToday(CredentialDto dto)
{
var allItems = await _context.MRRawPlans
.FromSqlRaw("EXEC GetAllAppointmentToday @UserId",
new SqlParameter("@UserId", dto.UserId))
.ToListAsync();
return allItems ?? new List<MRRawPlan>();
}
public async Task<List<MRRawPlan>> GetAllMissReschedule(CredentialDto dto)
{
var allItems = await _context.MRRawPlans
.FromSqlRaw("EXEC GetAllMissReschedule @UserId,@IsMissed,@IsReschedule",
new SqlParameter("@UserId", dto.UserId),
new SqlParameter("@IsMissed", dto.IsMissed),
new SqlParameter("@IsReschedule", dto.IsReschedule))
.ToListAsync();
return allItems ?? new List<MRRawPlan>();
}
public async Task<List<InventoryTransaction>> GetSampleTransaction(Guid mrRawPlanId)
{
var allItems = await _context.InventoryTransactions
.FromSqlRaw("EXEC GetSampleTransaction @MRRawPlanId",
new SqlParameter("@MRRawPlanId", mrRawPlanId)).ToListAsync();
return allItems ?? new List<InventoryTransaction>();
}
public async Task<List<ProductTransaction>> GetProductTransaction(Guid mrRawPlanId)
{
var allItems = await _context.ProductTransactions
.FromSqlRaw("EXEC GetProductTransaction @MRRawPlanId",
new SqlParameter("@MRRawPlanId", mrRawPlanId)).ToListAsync();
return allItems ?? new List<ProductTransaction>();
}
public async Task<List<MRRawPlansDto>> GetMRRawPlans(PlanByUserIdRequest dto)
{
var plans = await _context.MRRawPlansDto
.FromSqlRaw("EXEC GetMRRawPlans @UserId",
new SqlParameter("@UserId", dto.UserId)).ToListAsync();
return plans ?? new List<MRRawPlansDto>();
}
#endregion
}
}