429 lines
15 KiB
C#
429 lines
15 KiB
C#
using Aberwyn.Data;
|
|
using Aberwyn.Models;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Aberwyn.Controllers
|
|
{
|
|
[Authorize(Roles = "Budget")]
|
|
[ApiController]
|
|
[Route("api/budget")]
|
|
public class BudgetApiController : ControllerBase
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
|
|
public BudgetApiController(ApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
|
|
[HttpGet("{year:int}/{month:int}")]
|
|
public async Task<IActionResult> GetBudget(int year, int month)
|
|
{
|
|
try
|
|
{
|
|
var period = await _context.BudgetPeriods
|
|
.Include(p => p.Categories)
|
|
.ThenInclude(c => c.Items)
|
|
.FirstOrDefaultAsync(p => p.Year == year && p.Month == month);
|
|
|
|
if (period == null)
|
|
{
|
|
return Ok(new BudgetDto
|
|
{
|
|
Year = year,
|
|
Month = month,
|
|
Categories = new List<BudgetCategoryDto>()
|
|
});
|
|
}
|
|
|
|
|
|
var dto = new BudgetDto
|
|
{
|
|
Id = period.Id,
|
|
Year = period.Year,
|
|
Month = period.Month,
|
|
Categories = period.Categories
|
|
.OrderBy(cat => cat.Order)
|
|
.Select(cat => new BudgetCategoryDto
|
|
{
|
|
Id = cat.Id,
|
|
Name = cat.Name,
|
|
Color = cat.Color,
|
|
Items = cat.Items
|
|
.OrderBy(i => i.Order) // ← sortera innan mappning
|
|
.Select(i => new BudgetItemDto
|
|
{
|
|
Id = i.Id,
|
|
Name = i.Name,
|
|
Amount = i.Amount,
|
|
IsExpense = i.IsExpense,
|
|
IncludeInSummary = i.IncludeInSummary,
|
|
BudgetItemDefinitionId = i.BudgetItemDefinitionId
|
|
}).ToList()
|
|
}).ToList()
|
|
};
|
|
|
|
return Ok(dto);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, $"Fel: {ex.Message} \n{ex.StackTrace}");
|
|
}
|
|
}
|
|
|
|
[HttpPut("category/{id}")]
|
|
public async Task<IActionResult> UpdateCategory(int id, [FromBody] BudgetCategoryDto updatedCategory)
|
|
{
|
|
var category = await _context.BudgetCategories
|
|
.Include(c => c.Items)
|
|
.FirstOrDefaultAsync(c => c.Id == id);
|
|
|
|
if (category == null)
|
|
return NotFound();
|
|
|
|
// Hämta eller skapa kategori-definition
|
|
if (updatedCategory.BudgetCategoryDefinitionId.HasValue)
|
|
{
|
|
var def = await _context.BudgetCategoryDefinitions
|
|
.FirstOrDefaultAsync(d => d.Id == updatedCategory.BudgetCategoryDefinitionId.Value);
|
|
if (def != null)
|
|
{
|
|
category.BudgetCategoryDefinitionId = def.Id;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Om ingen ID angavs, försök hitta via namn
|
|
var def = await _context.BudgetCategoryDefinitions
|
|
.FirstOrDefaultAsync(d => d.Name.ToLower() == updatedCategory.Name.ToLower());
|
|
|
|
if (def == null)
|
|
{
|
|
def = new BudgetCategoryDefinition
|
|
{
|
|
Name = updatedCategory.Name,
|
|
Color = updatedCategory.Color ?? "#666666"
|
|
};
|
|
_context.BudgetCategoryDefinitions.Add(def);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
category.BudgetCategoryDefinitionId = def.Id;
|
|
}
|
|
|
|
// ✅ Uppdatera namn och färg (kan avvika från definitionens namn)
|
|
category.Name = updatedCategory.Name;
|
|
category.Color = updatedCategory.Color;
|
|
|
|
// Spara undan inkommande IDs (ignorera nya med Id = 0)
|
|
var incomingItemIds = updatedCategory.Items
|
|
.Where(i => i.Id != 0)
|
|
.Select(i => i.Id)
|
|
.ToList();
|
|
|
|
// Ta bort poster som inte längre finns i inkommande data
|
|
var itemsToRemove = category.Items
|
|
.Where(i => !incomingItemIds.Contains(i.Id))
|
|
.ToList();
|
|
|
|
_context.BudgetItems.RemoveRange(itemsToRemove);
|
|
|
|
// Lägg till nya poster eller uppdatera befintliga
|
|
foreach (var incoming in updatedCategory.Items)
|
|
{
|
|
if (incoming.Id == 0)
|
|
{
|
|
category.Items.Add(new BudgetItem
|
|
{
|
|
Name = incoming.Name,
|
|
Amount = incoming.Amount,
|
|
IsExpense = incoming.IsExpense,
|
|
IncludeInSummary = incoming.IncludeInSummary,
|
|
Order = incoming.Order,
|
|
BudgetCategoryId = category.Id
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var existing = category.Items.FirstOrDefault(i => i.Id == incoming.Id);
|
|
if (existing != null)
|
|
{
|
|
existing.Name = incoming.Name;
|
|
existing.Amount = incoming.Amount;
|
|
existing.IsExpense = incoming.IsExpense;
|
|
existing.IncludeInSummary = incoming.IncludeInSummary;
|
|
existing.Order = incoming.Order;
|
|
}
|
|
}
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpPut("category/order")]
|
|
public async Task<IActionResult> UpdateCategoryOrder([FromBody] List<BudgetCategoryDto> orderedCategories)
|
|
{
|
|
foreach (var dto in orderedCategories)
|
|
{
|
|
var cat = await _context.BudgetCategories.FindAsync(dto.Id);
|
|
if (cat != null)
|
|
cat.Order = dto.Order;
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
return NoContent();
|
|
}
|
|
|
|
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> CreatePeriod([FromBody] BudgetPeriod newPeriod)
|
|
{
|
|
_context.BudgetPeriods.Add(newPeriod);
|
|
await _context.SaveChangesAsync();
|
|
return CreatedAtAction(nameof(GetBudget), new { year = newPeriod.Year, month = newPeriod.Month }, newPeriod);
|
|
}
|
|
|
|
[HttpPut("item/{id}")]
|
|
public async Task<IActionResult> UpdateItem(int id, [FromBody] BudgetItem updatedItem)
|
|
{
|
|
var item = await _context.BudgetItems.FindAsync(id);
|
|
if (item == null) return NotFound();
|
|
|
|
item.Name = updatedItem.Name;
|
|
item.Amount = updatedItem.Amount;
|
|
item.IsExpense = updatedItem.IsExpense;
|
|
item.IncludeInSummary = updatedItem.IncludeInSummary;
|
|
item.Order = updatedItem.Order;
|
|
item.BudgetCategoryId = updatedItem.BudgetCategoryId;
|
|
|
|
await _context.SaveChangesAsync();
|
|
return Ok();
|
|
}
|
|
[HttpGet("definitions/items")]
|
|
public async Task<IActionResult> GetItemDefinitions()
|
|
{
|
|
var definitions = await _context.BudgetItemDefinitions
|
|
.OrderBy(d => d.Name)
|
|
.ToListAsync();
|
|
|
|
return Ok(definitions);
|
|
}
|
|
|
|
[HttpGet("definitions/categories")]
|
|
public async Task<IActionResult> GetCategoryDefinitions()
|
|
{
|
|
var definitions = await _context.BudgetCategoryDefinitions
|
|
.OrderBy(d => d.Name)
|
|
.ToListAsync();
|
|
|
|
return Ok(definitions);
|
|
}
|
|
|
|
|
|
[HttpPost("item")]
|
|
public async Task<IActionResult> CreateItem([FromBody] BudgetItem newItem)
|
|
{
|
|
if (newItem == null || newItem.BudgetCategoryId == 0 || string.IsNullOrWhiteSpace(newItem.Name))
|
|
return BadRequest("Ogiltig data.");
|
|
|
|
// ✅ Om BudgetItemDefinitionId är angiven, använd den
|
|
if (newItem.BudgetItemDefinitionId.HasValue && newItem.BudgetItemDefinitionId.Value > 0)
|
|
{
|
|
var existingDef = await _context.BudgetItemDefinitions
|
|
.FirstOrDefaultAsync(d => d.Id == newItem.BudgetItemDefinitionId);
|
|
|
|
if (existingDef == null)
|
|
return BadRequest("Ogiltigt definition-ID.");
|
|
|
|
// valfritt: du kan här också jämföra `newItem.Name != existingDef.Name`
|
|
// och t.ex. logga det som ett användarval av etikett.
|
|
}
|
|
else
|
|
{
|
|
// Om ID inte är angivet, sök på namn som fallback
|
|
var definition = await _context.BudgetItemDefinitions
|
|
.FirstOrDefaultAsync(d => d.Name.ToLower() == newItem.Name.ToLower());
|
|
|
|
if (definition == null)
|
|
{
|
|
definition = new BudgetItemDefinition
|
|
{
|
|
Name = newItem.Name,
|
|
IsExpense = newItem.IsExpense,
|
|
IncludeInSummary = newItem.IncludeInSummary
|
|
};
|
|
_context.BudgetItemDefinitions.Add(definition);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
newItem.BudgetItemDefinitionId = definition.Id;
|
|
}
|
|
|
|
_context.BudgetItems.Add(newItem);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return Ok(new { id = newItem.Id });
|
|
}
|
|
|
|
|
|
|
|
[HttpDelete("item/{id}")]
|
|
public async Task<IActionResult> DeleteItem(int id)
|
|
{
|
|
var item = await _context.BudgetItems.FindAsync(id);
|
|
if (item == null) return NotFound();
|
|
|
|
_context.BudgetItems.Remove(item);
|
|
await _context.SaveChangesAsync();
|
|
return NoContent();
|
|
}
|
|
[HttpDelete("{year:int}/{month:int}")]
|
|
public async Task<IActionResult> DeleteMonth(int year, int month)
|
|
{
|
|
var period = await _context.BudgetPeriods
|
|
.Include(p => p.Categories)
|
|
.ThenInclude(c => c.Items)
|
|
.FirstOrDefaultAsync(p => p.Year == year && p.Month == month);
|
|
|
|
if (period == null)
|
|
return NotFound();
|
|
|
|
// Ta bort alla items → kategorier → period
|
|
foreach (var category in period.Categories)
|
|
{
|
|
_context.BudgetItems.RemoveRange(category.Items);
|
|
}
|
|
|
|
_context.BudgetCategories.RemoveRange(period.Categories);
|
|
_context.BudgetPeriods.Remove(period);
|
|
|
|
await _context.SaveChangesAsync();
|
|
return NoContent();
|
|
}
|
|
|
|
|
|
[HttpPost("category")]
|
|
public async Task<IActionResult> CreateCategory([FromBody] BudgetCategoryDto newCategoryDto)
|
|
{
|
|
if (newCategoryDto == null || string.IsNullOrWhiteSpace(newCategoryDto.Name))
|
|
return BadRequest("Ogiltig data.");
|
|
|
|
var period = await _context.BudgetPeriods
|
|
.FirstOrDefaultAsync(p => p.Year == newCategoryDto.Year && p.Month == newCategoryDto.Month);
|
|
|
|
if (period == null)
|
|
{
|
|
period = new BudgetPeriod
|
|
{
|
|
Year = newCategoryDto.Year,
|
|
Month = newCategoryDto.Month
|
|
};
|
|
_context.BudgetPeriods.Add(period);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
var definition = await _context.BudgetCategoryDefinitions
|
|
.FirstOrDefaultAsync(d => d.Name.ToLower() == newCategoryDto.Name.ToLower());
|
|
|
|
if (definition == null)
|
|
{
|
|
definition = new BudgetCategoryDefinition
|
|
{
|
|
Name = newCategoryDto.Name,
|
|
Color = newCategoryDto.Color ?? "#666666"
|
|
};
|
|
_context.BudgetCategoryDefinitions.Add(definition);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
var category = new BudgetCategory
|
|
{
|
|
Name = newCategoryDto.Name,
|
|
Color = newCategoryDto.Color ?? definition.Color,
|
|
BudgetPeriodId = period.Id,
|
|
Order = newCategoryDto.Order,
|
|
BudgetCategoryDefinitionId = definition.Id
|
|
};
|
|
|
|
_context.BudgetCategories.Add(category);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return Ok(new { id = category.Id });
|
|
}
|
|
|
|
[HttpDelete("category/{id}")]
|
|
public async Task<IActionResult> DeleteCategory(int id)
|
|
{
|
|
var category = await _context.BudgetCategories
|
|
.Include(c => c.Items)
|
|
.FirstOrDefaultAsync(c => c.Id == id);
|
|
|
|
if (category == null)
|
|
return NotFound();
|
|
|
|
_context.BudgetItems.RemoveRange(category.Items);
|
|
_context.BudgetCategories.Remove(category);
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpPost("copy/{year:int}/{month:int}")]
|
|
public async Task<IActionResult> CopyFromPreviousMonth(int year, int month)
|
|
{
|
|
var targetPeriod = await _context.BudgetPeriods
|
|
.Include(p => p.Categories)
|
|
.ThenInclude(c => c.Items)
|
|
.FirstOrDefaultAsync(p => p.Year == year && p.Month == month);
|
|
|
|
if (targetPeriod != null && targetPeriod.Categories.Any())
|
|
return BadRequest("Det finns redan data för denna månad.");
|
|
|
|
var previous = new DateTime(year, month, 1).AddMonths(-1);
|
|
var previousPeriod = await _context.BudgetPeriods
|
|
.Include(p => p.Categories)
|
|
.ThenInclude(c => c.Items)
|
|
.FirstOrDefaultAsync(p => p.Year == previous.Year && p.Month == previous.Month);
|
|
|
|
if (previousPeriod == null)
|
|
return NotFound("Ingen data att kopiera från.");
|
|
|
|
var newPeriod = new BudgetPeriod
|
|
{
|
|
Year = year,
|
|
Month = month,
|
|
Categories = previousPeriod.Categories.Select(cat => new BudgetCategory
|
|
{
|
|
Name = cat.Name,
|
|
Color = cat.Color,
|
|
Order = cat.Order,
|
|
BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId, // 🟢 Lägg till denna rad
|
|
Items = cat.Items.Select(item => new BudgetItem
|
|
{
|
|
Name = item.Name,
|
|
Amount = item.Amount,
|
|
IsExpense = item.IsExpense,
|
|
IncludeInSummary = item.IncludeInSummary,
|
|
Order = item.Order,
|
|
BudgetItemDefinitionId = item.BudgetItemDefinitionId // 🟢 Lägg till denna rad
|
|
}).ToList()
|
|
}).ToList()
|
|
};
|
|
|
|
_context.BudgetPeriods.Add(newPeriod);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return Ok();
|
|
}
|
|
|
|
|
|
|
|
}
|
|
}
|