10.
Implementation A partner, not just software HexDive Technologies' local team handles setup, data migration, training and ongoing support — in Sinhala, Tamil and English. [confirm] 1 Discovery Map your network, products and processes. 2 Configuration Set up depots, pricing, schemes, routes and users. 3 Migration & training Move your data and train every team. 4 Go-live & support Dedicated account manager and local support desk.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/expense-categories")]
|
||||
public class ExpenseCategoryController(ExpenseCategoryService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize("expense.view")]
|
||||
public async Task<IActionResult> Tree() => Ok(await service.GetTreeAsync());
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("expense.manage_categories")]
|
||||
public async Task<IActionResult> Create(CreateExpenseCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize("expense.manage_categories")]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateExpenseCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.UpdateAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public record ExpenseCategoryResponse(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Code,
|
||||
Guid? ParentCategoryId,
|
||||
bool IsActive,
|
||||
List<ExpenseCategoryResponse> Children);
|
||||
|
||||
public record CreateExpenseCategoryRequest(
|
||||
string Name,
|
||||
string? Code,
|
||||
Guid? ParentCategoryId);
|
||||
|
||||
public record UpdateExpenseCategoryRequest(
|
||||
string Name,
|
||||
string? Code,
|
||||
bool IsActive);
|
||||
@@ -0,0 +1,19 @@
|
||||
using Distribution.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public class ExpenseCategoryRepository(AppDBContext context) : IExpenseCategoryRepository
|
||||
{
|
||||
public async Task<Models.ExpenseCategory?> GetByIdAsync(Guid id) =>
|
||||
await context.ExpenseCategories.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
public async Task<List<Models.ExpenseCategory>> GetAllAsync() =>
|
||||
await context.ExpenseCategories.OrderBy(c => c.FK_ParentCategoryId).ThenBy(c => c.Name).ToListAsync();
|
||||
|
||||
public async Task AddAsync(Models.ExpenseCategory entity) =>
|
||||
await context.ExpenseCategories.AddAsync(entity);
|
||||
|
||||
public void Update(Models.ExpenseCategory entity) =>
|
||||
context.ExpenseCategories.Update(entity);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Distribution.Infra.UoW;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public class ExpenseCategoryService(IExpenseCategoryRepository repo, IUnitOfWork uow)
|
||||
{
|
||||
public async Task<List<ExpenseCategoryResponse>> GetTreeAsync()
|
||||
{
|
||||
var all = await repo.GetAllAsync();
|
||||
var byParent = all.ToLookup(c => c.FK_ParentCategoryId);
|
||||
|
||||
return all
|
||||
.Where(c => c.FK_ParentCategoryId is null)
|
||||
.Select(c => Map(c, byParent))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static ExpenseCategoryResponse Map(Models.ExpenseCategory c, ILookup<Guid?, Models.ExpenseCategory> byParent) =>
|
||||
new(c.Id, c.Name, c.Code, c.FK_ParentCategoryId, c.IsActive,
|
||||
byParent[c.Id].Select(child => Map(child, byParent)).ToList());
|
||||
|
||||
public async Task<ExpenseCategoryResponse> CreateAsync(CreateExpenseCategoryRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
throw new InvalidOperationException("Name is required");
|
||||
|
||||
if (request.ParentCategoryId is { } parentId)
|
||||
{
|
||||
var parent = await repo.GetByIdAsync(parentId)
|
||||
?? throw new InvalidOperationException("Parent category not found");
|
||||
if (parent.FK_ParentCategoryId is not null)
|
||||
throw new InvalidOperationException("Only two levels are allowed — cannot nest under a title");
|
||||
}
|
||||
|
||||
var entity = new Models.ExpenseCategory
|
||||
{
|
||||
Name = request.Name.Trim(),
|
||||
Code = string.IsNullOrWhiteSpace(request.Code) ? null : request.Code.Trim(),
|
||||
FK_ParentCategoryId = request.ParentCategoryId
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { await repo.AddAsync(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return new ExpenseCategoryResponse(entity.Id, entity.Name, entity.Code, entity.FK_ParentCategoryId, entity.IsActive, []);
|
||||
}
|
||||
|
||||
public async Task<ExpenseCategoryResponse?> UpdateAsync(Guid id, UpdateExpenseCategoryRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
|
||||
entity.Name = request.Name.Trim();
|
||||
entity.Code = string.IsNullOrWhiteSpace(request.Code) ? null : request.Code.Trim();
|
||||
entity.IsActive = request.IsActive;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return new ExpenseCategoryResponse(entity.Id, entity.Name, entity.Code, entity.FK_ParentCategoryId, entity.IsActive, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public interface IExpenseCategoryRepository
|
||||
{
|
||||
Task<Models.ExpenseCategory?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.ExpenseCategory>> GetAllAsync();
|
||||
Task AddAsync(Models.ExpenseCategory entity);
|
||||
void Update(Models.ExpenseCategory entity);
|
||||
}
|
||||
Reference in New Issue
Block a user