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, IPlanningRepo { private readonly LSFEDbContext _context; public PlanningRepo(LSFEDbContext context) : base(context) { _context = context; } #region Post Put public async Task 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 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 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 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 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(); 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 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 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 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 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 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> 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(); } public async Task> GetPlan(PlanningDto dto) { var allItems = await _context.MRRawPlans .FromSqlRaw("EXEC GetPlan @UserId", new SqlParameter("@UserId", dto.UserId)) .ToListAsync(); return allItems ?? new List(); } public async Task> 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(); } public async Task> 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(); } 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> GetAllAppointmentToday(CredentialDto dto) { var allItems = await _context.MRRawPlans .FromSqlRaw("EXEC GetAllAppointmentToday @UserId", new SqlParameter("@UserId", dto.UserId)) .ToListAsync(); return allItems ?? new List(); } public async Task> 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(); } public async Task> GetSampleTransaction(Guid mrRawPlanId) { var allItems = await _context.InventoryTransactions .FromSqlRaw("EXEC GetSampleTransaction @MRRawPlanId", new SqlParameter("@MRRawPlanId", mrRawPlanId)).ToListAsync(); return allItems ?? new List(); } public async Task> GetProductTransaction(Guid mrRawPlanId) { var allItems = await _context.ProductTransactions .FromSqlRaw("EXEC GetProductTransaction @MRRawPlanId", new SqlParameter("@MRRawPlanId", mrRawPlanId)).ToListAsync(); return allItems ?? new List(); } public async Task> GetMRRawPlans(PlanByUserIdRequest dto) { var plans = await _context.MRRawPlansDto .FromSqlRaw("EXEC GetMRRawPlans @UserId", new SqlParameter("@UserId", dto.UserId)).ToListAsync(); return plans ?? new List(); } #endregion } }