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;
}