using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Core.Interfaces; using PowderCoating.Shared.Constants; namespace PowderCoating.Web.Controllers; /// /// Manages annual budgets. Each budget has one BudgetLine per active GL account with /// monthly amounts (Jan–Dec). The Budget vs. Actual report compares these to real activity. /// Only one budget per year is marked IsDefault — that one feeds the variance report automatically. /// [Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)] public class BudgetsController : Controller { private readonly IUnitOfWork _unitOfWork; private readonly ITenantContext _tenantContext; public BudgetsController(IUnitOfWork unitOfWork, ITenantContext tenantContext) { _unitOfWork = unitOfWork; _tenantContext = tenantContext; } // ── Index ───────────────────────────────────────────────────────────────── /// Lists all budgets for the current company ordered by fiscal year descending. public async Task Index() { var budgets = (await _unitOfWork.Budgets.FindAsync(b => true, false, b => b.Lines)) .OrderByDescending(b => b.FiscalYear) .ThenBy(b => b.Name) .ToList(); return View(budgets); } // ── Create ──────────────────────────────────────────────────────────────── [HttpGet] public async Task Create() { var accounts = await GetBudgetableAccountsAsync(); return View(new BudgetCreateVm { FiscalYear = DateTime.Now.Year, Lines = accounts.Select(a => new BudgetLineVm { AccountId = a.Id, AccountNumber = a.AccountNumber, AccountName = a.Name, AccountType = a.AccountType }).ToList() }); } [HttpPost, ValidateAntiForgeryToken] public async Task Create(BudgetCreateVm vm) { if (!ModelState.IsValid) return View(vm); var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; // If this is marked default, clear the flag on other budgets for the same year if (vm.IsDefault) await ClearDefaultFlagAsync(companyId, vm.FiscalYear, excludeId: null); var budget = new Budget { Name = vm.Name, FiscalYear = vm.FiscalYear, Notes = vm.Notes, IsDefault = vm.IsDefault, CompanyId = companyId, CreatedAt = DateTime.UtcNow, Lines = vm.Lines .Where(l => l.HasAnyAmount) .Select(l => new BudgetLine { AccountId = l.AccountId, Jan = l.Jan, Feb = l.Feb, Mar = l.Mar, Apr = l.Apr, May = l.May, Jun = l.Jun, Jul = l.Jul, Aug = l.Aug, Sep = l.Sep, Oct = l.Oct, Nov = l.Nov, Dec = l.Dec, CompanyId = companyId, CreatedAt = DateTime.UtcNow }).ToList() }; await _unitOfWork.Budgets.AddAsync(budget); await _unitOfWork.CompleteAsync(); TempData["Success"] = $"Budget \"{budget.Name}\" created for {budget.FiscalYear}."; return RedirectToAction(nameof(Edit), new { id = budget.Id }); } // ── Edit ────────────────────────────────────────────────────────────────── [HttpGet] public async Task Edit(int id) { var budget = await _unitOfWork.Budgets.GetByIdAsync(id, false, b => b.Lines); if (budget == null) return NotFound(); var accounts = await GetBudgetableAccountsAsync(); var lineMap = budget.Lines.ToDictionary(l => l.AccountId); var vm = new BudgetCreateVm { Id = budget.Id, Name = budget.Name, FiscalYear = budget.FiscalYear, Notes = budget.Notes, IsDefault = budget.IsDefault, Lines = accounts.Select(a => { lineMap.TryGetValue(a.Id, out var existing); return new BudgetLineVm { AccountId = a.Id, AccountNumber = a.AccountNumber, AccountName = a.Name, AccountType = a.AccountType, Jan = existing?.Jan ?? 0, Feb = existing?.Feb ?? 0, Mar = existing?.Mar ?? 0, Apr = existing?.Apr ?? 0, May = existing?.May ?? 0, Jun = existing?.Jun ?? 0, Jul = existing?.Jul ?? 0, Aug = existing?.Aug ?? 0, Sep = existing?.Sep ?? 0, Oct = existing?.Oct ?? 0, Nov = existing?.Nov ?? 0, Dec = existing?.Dec ?? 0 }; }).ToList() }; return View(vm); } [HttpPost, ValidateAntiForgeryToken] public async Task Edit(int id, BudgetCreateVm vm) { if (id != vm.Id) return BadRequest(); if (!ModelState.IsValid) return View(vm); var budget = await _unitOfWork.Budgets.GetByIdAsync(id, false, b => b.Lines); if (budget == null) return NotFound(); var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; if (vm.IsDefault && !budget.IsDefault) await ClearDefaultFlagAsync(companyId, vm.FiscalYear, excludeId: id); budget.Name = vm.Name; budget.Notes = vm.Notes; budget.IsDefault = vm.IsDefault; budget.UpdatedAt = DateTime.UtcNow; // Delete old lines and replace with new set (simpler than merge) foreach (var line in budget.Lines.ToList()) await _unitOfWork.BudgetLines.SoftDeleteAsync(line.Id); budget.Lines = vm.Lines .Where(l => l.HasAnyAmount) .Select(l => new BudgetLine { AccountId = l.AccountId, Jan = l.Jan, Feb = l.Feb, Mar = l.Mar, Apr = l.Apr, May = l.May, Jun = l.Jun, Jul = l.Jul, Aug = l.Aug, Sep = l.Sep, Oct = l.Oct, Nov = l.Nov, Dec = l.Dec, CompanyId = companyId, CreatedAt = DateTime.UtcNow }).ToList(); await _unitOfWork.CompleteAsync(); TempData["Success"] = $"Budget \"{budget.Name}\" saved."; return RedirectToAction(nameof(Edit), new { id }); } // ── Copy ───────────────────────────────────────────────────────────────── /// /// Creates a copy of an existing budget for a new fiscal year — common workflow for /// rolling forward last year's budget as a starting point. /// [HttpPost, ValidateAntiForgeryToken] public async Task Copy(int id, int newYear) { var source = await _unitOfWork.Budgets.GetByIdAsync(id, false, b => b.Lines); if (source == null) return NotFound(); var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; var copy = new Budget { Name = $"{source.Name} ({newYear})", FiscalYear = newYear, Notes = source.Notes, IsDefault = false, CompanyId = companyId, CreatedAt = DateTime.UtcNow, Lines = source.Lines.Select(l => new BudgetLine { AccountId = l.AccountId, Jan = l.Jan, Feb = l.Feb, Mar = l.Mar, Apr = l.Apr, May = l.May, Jun = l.Jun, Jul = l.Jul, Aug = l.Aug, Sep = l.Sep, Oct = l.Oct, Nov = l.Nov, Dec = l.Dec, CompanyId = companyId, CreatedAt = DateTime.UtcNow }).ToList() }; await _unitOfWork.Budgets.AddAsync(copy); await _unitOfWork.CompleteAsync(); TempData["Success"] = $"Budget copied to {newYear}."; return RedirectToAction(nameof(Edit), new { id = copy.Id }); } // ── SetDefault ──────────────────────────────────────────────────────────── [HttpPost, ValidateAntiForgeryToken] public async Task SetDefault(int id) { var budget = await _unitOfWork.Budgets.GetByIdAsync(id); if (budget == null) return NotFound(); var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; await ClearDefaultFlagAsync(companyId, budget.FiscalYear, excludeId: null); budget.IsDefault = true; budget.UpdatedAt = DateTime.UtcNow; await _unitOfWork.CompleteAsync(); TempData["Success"] = $"\"{budget.Name}\" is now the default budget for {budget.FiscalYear}."; return RedirectToAction(nameof(Index)); } // ── Delete ──────────────────────────────────────────────────────────────── [HttpPost, ValidateAntiForgeryToken] public async Task Delete(int id) { var budget = await _unitOfWork.Budgets.GetByIdAsync(id, false, b => b.Lines); if (budget == null) return NotFound(); foreach (var line in budget.Lines.ToList()) await _unitOfWork.BudgetLines.SoftDeleteAsync(line.Id); await _unitOfWork.Budgets.SoftDeleteAsync(id); await _unitOfWork.CompleteAsync(); TempData["Success"] = $"Budget \"{budget.Name}\" deleted."; return RedirectToAction(nameof(Index)); } // ── Helpers ─────────────────────────────────────────────────────────────── private async Task> GetBudgetableAccountsAsync() { var accounts = await _unitOfWork.Accounts.FindAsync( a => a.IsActive && (a.AccountType == AccountType.Revenue || a.AccountType == AccountType.Expense)); return accounts.OrderBy(a => a.AccountNumber).ToList(); } private async Task ClearDefaultFlagAsync(int companyId, int fiscalYear, int? excludeId) { var others = await _unitOfWork.Budgets.FindAsync( b => b.IsDefault && b.FiscalYear == fiscalYear && b.Id != (excludeId ?? 0)); foreach (var b in others) { b.IsDefault = false; b.UpdatedAt = DateTime.UtcNow; } if (others.Any()) await _unitOfWork.CompleteAsync(); } } // ── View Models ─────────────────────────────────────────────────────────────── public class BudgetCreateVm { public int Id { get; set; } public string Name { get; set; } = string.Empty; public int FiscalYear { get; set; } = DateTime.Now.Year; public string? Notes { get; set; } public bool IsDefault { get; set; } = true; public List Lines { get; set; } = new(); } public class BudgetLineVm { public int AccountId { get; set; } public string AccountNumber { get; set; } = string.Empty; public string AccountName { get; set; } = string.Empty; public AccountType AccountType { get; set; } public decimal Jan { get; set; } public decimal Feb { get; set; } public decimal Mar { get; set; } public decimal Apr { get; set; } public decimal May { get; set; } public decimal Jun { get; set; } public decimal Jul { get; set; } public decimal Aug { get; set; } public decimal Sep { get; set; } public decimal Oct { get; set; } public decimal Nov { get; set; } public decimal Dec { get; set; } public decimal Annual => Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec; public bool HasAnyAmount => Annual != 0; }